Skip to content
Edit on GitHub

Author transient surface wetness and HDRP rain-gloss response

verifiedAgainst b1311b5 · verifiedOn 2026-09-10.

In Voxamine, environmental weather does not merely spawn overhead raindrop particles. When rain falls across voxel terrain, the ground physically reflects that moisture (MASTERPLAN.md §13.1): blocks visibly darken from liquid saturation, specular highlights sharpen across stone and soil as thin water films accumulate, and sun and moon directional highlights travel smoothly across pooled surface gloss.

You will implement the transient surface moisture pipeline using SurfaceWetnessMath.cs, differentiate fast surface films from slow biological soil moisture (SoilMoistureMath.cs), configure biome-dependent drying rates via BiomeWeatherProfile.cs, map global wetness into an HDRP Lit albedo darkening and specular response curve using WetSurfacePresentationMath.cs, and drive terrain materials self-bootstrappingly via SurfaceWetnessRuntime.cs.

// Evaluate instantaneous surface material look during a downpour:
WetSurfaceMaterialLook look = WetSurfacePresentationMath.Evaluate(
authoredBaseColor: stoneColor,
authoredSmoothness: 0.15f,
globalWetness: 0.85f,
materialResponse: 1.0f);
// Albedo darkens by 28% from water film saturation:
// look.BaseColor -> darkened stone RGB
// Smoothness climbs to mirror-like wet specular sheen:
// look.Smoothness -> 0.76f (sharpened HDRP highlight!)

The surface response architecture decouples transient visual presentation from persistent world data:

  • Weather Engine (WeatherRuntime & WeatherSnapshot) publishes the current weather state (Rain, Snow, Clear) and precipitation intensity.
  • Pure Film Math (SurfaceWetnessMath) integrates rain soaking ($+0.11/\text{s}$) and arid evaporation without allocating memory or touching game objects.
  • Presentation Finishing (WetSurfacePresentationMath) translates scalar wetness and snow cover into non-destructive HDRP material modifications.
  • Runtime Coordinator (SurfaceWetnessRuntime) samples the player’s active biome, evaluates wetness over delta time, and updates material properties on active chunk renderers.
flowchart TD
  subgraph SIMULATION["Weather Simulation"]
    WEATHER["WeatherRuntime.CurrentSnapshot\n(WeatherType, PrecipitationIntensity)"]
    BIOME["BiomeWeatherProfile.For(biome)\n(PrecipitationSusceptibility, Freezes)"]
  end

  subgraph MATH["VoxelSandbox.World.Environment"]
    FILM["SurfaceWetnessMath.Advance\n(+0.11/s rain soak, -0.028/s base dry)"]
    GLOSS["SurfaceWetnessMath.EvaluateSurfaceGloss\n(SmoothStep [0.05, 0.8] ramp)"]
    LOOK["WetSurfacePresentationMath.Evaluate\n(Albedo darkening 28%, Max smoothness 0.76)"]
  end

  subgraph RUNTIME["SurfaceWetnessRuntime (MonoBehaviour)"]
    UPDATE["Update loop (0.5s refresh timer)\nGlobalSurfaceWetness ∈ [0, 1]"]
    MAT_CACHE["Dictionary<Material, WetMaterialState>\n(Preserves authored albedo & smoothness)"]
  end

  subgraph HDRP["HDRP Chunk Rendering"]
    TERRAIN["Terrain Chunk Mesh Renderers\n(_BaseColor, _Smoothness modulated)"]
  end

  WEATHER --> UPDATE
  BIOME --> UPDATE
  UPDATE --> FILM
  FILM --> GLOSS
  GLOSS --> LOOK
  LOOK --> MAT_CACHE
  MAT_CACHE --> TERRAIN

Read Docs/MASTERPLAN.md §13.1 (“Weather and surface response”), then inspect:

Understand the distinction between the two moisture layers:

Layer Type Timescale Persistence Effect
Surface Wetness (SurfaceWetnessMath) Physical surface liquid film Minutes (soaks in $10\text{ s}$, dries in $90\text{ s}$) Transient (not persisted) Darkens block albedo by up to $28%$, sharpens specular highlights to $0.76$ smoothness.
Soil Moisture (SoilMoistureMath) Subsurface capillary ground water In-game days Persisted in save data Drives crop growth rates, seed germination, and arid vegetation death.

1. Differential Film Accumulation and Drying

Section titled “1. Differential Film Accumulation and Drying”

In SurfaceWetnessMath.Advance, advance the surface film $w \in [0, 1]$ over elapsed delta time:

$$\frac{dw}{dt} = \begin{cases} +0.11 \times I_{\text{precip}} & \text{if Rain} \ +0.02 \times I_{\text{precip}} & \text{if Snow} \ -0.028 \times \text{lerp}(0.55, 2.3, d_{\text{biome}}) & \text{if Clear} \end{cases}$$

public static float Advance(float current, WeatherType weather, float precipitationIntensity, float biomeDryness, float deltaSeconds)
{
float wetness = Mathf.Clamp01(current);
if (float.IsNaN(deltaSeconds) || float.IsInfinity(deltaSeconds) || deltaSeconds <= 0f)
{
return wetness;
}
float intensity = Mathf.Clamp01(precipitationIntensity);
float rate = weather switch
{
WeatherType.Rain => RainSoakPerSecond * intensity,
WeatherType.Snow => SnowSoakPerSecond * intensity,
_ => -BaseDryPerSecond * Mathf.Lerp(0.55f, 2.3f, Mathf.Clamp01(biomeDryness))
};
return Mathf.Clamp01(wetness + rate * deltaSeconds);
}

Notice that drying is modulated by BiomeDryness: arid deserts evaporate wet films up to $4\times$ faster than humid rainforests or cold taigas.

Linear wetness does not produce a natural optical response: puddles form distinct glossy patches rather than a foggy blur. In SurfaceWetnessMath.EvaluateSurfaceGloss, map scalar wetness through a smoothstep S-curve:

public static float EvaluateSurfaceGloss(float wetness)
{
// Ignores the first 5% dampness; saturates into full puddle gloss at 80%:
return Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0.05f, 0.8f, Mathf.Clamp01(wetness)));
}

In WetSurfacePresentationMath.Evaluate, calculate the finished material look:

public static WetSurfaceMaterialLook Evaluate(
Color authoredBaseColor, float authoredSmoothness,
float globalWetness, float wetResponse,
float globalSnowCover, float snowResponse)
{
float wr = Mathf.Clamp01(wetResponse);
float wetness = Mathf.Clamp01(globalWetness) * wr;
float gloss = SurfaceWetnessMath.EvaluateSurfaceGloss(globalWetness) * wr;
// Darken albedo by up to 28%:
float darkening = 1f - WetDarkening * wetness;
Color color = new(
authoredBaseColor.r * darkening,
authoredBaseColor.g * darkening,
authoredBaseColor.b * darkening,
authoredBaseColor.a);
// Boost smoothness toward 0.76 wet sheen:
float smoothness = Mathf.Lerp(authoredSmoothness, MaximumWetSmoothness, gloss);
// Lying snow overlays the wet ground:
float sr = Mathf.Clamp01(snowResponse);
float snow = Mathf.Clamp01(globalSnowCover) * sr;
if (snow > 0f)
{
color = Color.Lerp(color, SnowColor, snow);
smoothness = Mathf.Lerp(smoothness, SnowSmoothness, snow);
}
return new WetSurfaceMaterialLook(color, smoothness);
}

In SurfaceWetnessRuntime.cs, materials are cached upon discovery so authored values are never permanently overwritten:

if (!wetMaterials.TryGetValue(mat, out WetMaterialState state))
{
state = new WetMaterialState(
mat.color,
mat.GetFloat("_Smoothness"),
ResolveWetResponse(mat));
wetMaterials.Add(mat, state);
}
WetSurfaceMaterialLook look = WetSurfacePresentationMath.Evaluate(
state.AuthoredColor, state.AuthoredSmoothness, wetness, state.Response);
mat.color = look.BaseColor;
mat.SetFloat("_Smoothness", look.Smoothness);

Because the runtime relaxes to equilibrium within 90 seconds, it requires zero disk persistence. A save game reloads at dry state and naturally reconverges if loaded during an active storm.

Run the dedicated surface response test suite:

Terminal window
/opt/unity/Editor/Unity -batchmode -nographics \
-projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \
-runTests -testPlatform EditMode \
-testFilter VoxelSandbox.Tests.SurfaceWetnessMathTests
Terminal window
/opt/unity/Editor/Unity -batchmode -nographics \
-projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \
-runTests -testPlatform EditMode \
-testFilter VoxelSandbox.Tests.WetSurfacePresentationMathTests

Verify that all assertions pass:

  • Advance_RainSoaksTowardFullyWet: verifies steady-state convergence at $1.0$.
  • Advance_ClearWeatherDriesTowardZero: verifies complete evaporation after rain stops.
  • Advance_AridBiomesDryFasterThanHumidOnes: validates desert vs jungle evaporation multipliers.
  • Evaluate_DarkensAlbedoAndRampsSmoothness: verifies the $28%$ darkening and $0.76$ max smoothness boundaries.
  • Evaluate_SnowOverridesWetLook: proves lying snow covers the wet surface look.
  1. Enter Play Mode in Gameplay.unity.
  2. Locate [Voxel Sandbox] Surface Wetness in the scene hierarchy.
  3. Observe GlobalSurfaceWetness during clear weather: reads 0.0.
  4. Trigger rain via the debug console or Weather Lab:
    • Watch GlobalSurfaceWetness climb smoothly from 0.0 to 1.0 over ~10 seconds.
    • Observe ground terrain darkening and specular glints appearing as sun highlights hit the wet surface.
  5. Stop rain: observe the ground gradually brightening back to its authored matte appearance.

To author a custom block material with specialized wetness response (e.g. Porous Sandstone or Water-Repellent Waxed Planks):

  1. Configure Material Response: Set custom _WetResponse float on the material ($0.0$ for waterproof, $1.0$ for full soak).
  2. Add Porosity Overrides: Extend SurfaceWetnessRuntime.ResolveWetResponse to query block properties from BlockDefinition.
  3. Verify Restoration: Confirm that entering and exiting rain fully restores authored base color without drift.
  4. Add EditMode Tests: Add test assertions in WetSurfacePresentationMathTests.cs.
Symptom Cause Fix
Save file size grows or save schema breaks across updates. Surface wetness was saved to WorldSaveData.bin. Do not persist transient surface wetness. The differential equations converge to steady state within 90 seconds.
Ground turns completely black or blindingly specular during light rain. EvaluateSurfaceGloss was mapped with a linear function instead of smoothstep. Use Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0.05f, 0.8f, wetness)) to filter out minor dampness before sharpening highlights.
Crops stop growing during dry weather even with high surface wetness. Plant growth was wired to SurfaceWetnessMath instead of SoilMoistureMath. Surface wetness is a transient visual film. Deep biological moisture belongs exclusively to SoilMoistureMath.
Material base colors become permanently darkened across scene reloads. mat.color was modified without storing the initial authored color in a cache. Always cache state.AuthoredColor before applying weather adjustments.
Wetness updates hitch or spike CPU frame time in large worlds. Every chunk material was queried and modified on every single frame. Throttle updates using RefreshIntervalSeconds = 0.5f as implemented in SurfaceWetnessRuntime.

User-contributed notes

Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works: **bold**, *italic*, `code`, and links.

Notes policy

Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked nofollow.

  1. Loading notes…