Skip to content
Back to skills

Realtime Wet Surfaces

ASecurity

Real-time wetness on existing materials without losing their values — value-copying wet shader, geometry-driven wet mask, instance-uniform wet amount.

  • 77 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentsgonode

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add SummerEngine/summer --skill realtime-wet-surfaces --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Realtime Wet Surfaces?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Realtime Wet Surfaces
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/summerengine-realtime-wet-surfaces/badge)](https://www.skillsdirectory.com/skills/summerengine-realtime-wet-surfaces)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: realtime-wet-surfaces
description: "Real-time wetness on existing materials without losing their values — value-copying wet shader, geometry-driven wet mask, instance-uniform wet amount."
license: MIT
category: shaders
tags:
  - rendering
  - shaders
  - godot4
  - weather
  - materials
confidence: extracted
---

# Real-Time Wet Surfaces (Value-Preserving Wet Materials)

## Outcome

Implement a real-time wetness effect in Summer Engine that can be applied to existing materials **without losing their configured values** (albedo, metallic, roughness, textures). The wet mask is driven by geometry alone (upward-facing surfaces collect wetness first), with a global/per-instance wet amount that can be animated at runtime. This follows the source technique: a pre-configured shader swap that keeps the standard material's values, fed by a custom-function bridge.

## When to Use

- Rain, wet caves, shorelines, or post-rain scenes where surfaces should darken and become glossy in real time.
- You need to wet many existing materials without re-authoring each one or losing its parameter values.
- You want a wetness amount that a gameplay system (rain zone, weather controller) can drive at runtime.

## Core Principle

The source technique's key idea is **separation of wetness from material authoring**: the wet logic is a self-contained graph/function that receives material values and injects a wet mask, so enabling wetness is a swap, not a rewrite.

How the source concepts map to Summer Engine:

| Source concept | Summer Engine equivalent |
|---|---|
| Standard lit material with retained values | `StandardMaterial3D` values copied into a `ShaderMaterial` by a one-time converter |
| Pre-configured shader-graph swap | Shared wet-enabled `.gdshader`; swap the material once, keep all parameter values |
| Custom-function node carrying values into the graph | Shader uniforms + instance uniforms (`set_instance_shader_parameter`) or `RenderingServer` global shader parameters for scene-wide wet state |
| Wet mask from geometry shape alone | Fragment mask from world-space normal up-facing component (`world_normal.y`), optionally broken up by world-position noise |
| Planned heightmap flow | Sample a heightmap texture and offset the wet mask along its gradient (future extension; not evidenced in source) |

**Constraint:** the engine cannot inject custom nodes into a `StandardMaterial3D` at runtime. The equivalent of "swap in the pre-configured shader while keeping all values" is a **converter that copies `StandardMaterial3D` parameters into the wet shader** (below). The alternative — leaving `StandardMaterial3D` untouched and overlaying wetness via decals or post-processing — does not match the source's material-preserving architecture and is not recommended here.

## Scene / Node Shape

```
Scene
├── Node3D (level)
│   ├── MeshInstance3D × N
│   │     └── Surface overrides: ShaderMaterial (wet-enabled shader, one shared .gdshader)
│   │           instance uniform `wet_amount` driven per object or globally
│   └── RainZone: Area3D (optional)
│         └── body_entered/exited signals → WetnessController
└── WetnessController: Node
      └── typed GDScript; animates wet_amount up while raining, down while drying
```

## Implementation Steps

### 1. Write the shared wet-enabled spatial shader

Preserve the PBR parameters as uniforms and compute the wet mask from geometry. Fragment-stage `NORMAL` is **view-space**, so pass a world-space normal from the vertex stage — otherwise the "upward-facing" mask rotates with the camera.

```glsl
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back;

// Values copied from the original StandardMaterial3D by the converter
uniform vec4 albedo_color : source_color = vec4(1.0);
uniform sampler2D albedo_tex : filter_linear_mipmap;
uniform float metallic : hint_range(0.0, 1.0) = 0.0;
uniform float roughness : hint_range(0.0, 1.0) = 0.5;

// Wet state
instance uniform float wet_amount : hint_range(0.0, 1.0) = 0.0;
uniform float up_bias : hint_range(0.0, 1.0) = 0.35;    // wetness starts below fully-upward
uniform float darkening : hint_range(0.0, 1.0) = 0.55;  // how much wet albedo darkens
uniform float wet_roughness : hint_range(0.0, 1.0) = 0.08;
uniform float noise_scale = 8.0;
uniform sampler2D droplet_noise : filter_linear_mipmap, repeat_enable;

varying vec3 world_normal;
varying vec3 world_pos;

void vertex() {
    world_normal = (MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz;
    world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}

void fragment() {
    vec4 alb := texture(albedo_tex, UV) * albedo_color;
    vec3 wn := normalize(world_normal);

    // Geometry-driven wet mask: upward-facing surfaces collect wetness first
    float facing := clamp(wn.y, 0.0, 1.0);
    float mask := smoothstep(up_bias, 1.0, facing) * wet_amount;

    // Break up the mask so it reads as weathering, not a flat shader pass
    mask *= 0.75 + 0.25 * texture(droplet_noise, world_pos.xz * noise_scale).r;

    ALBEDO = mix(alb.rgb, alb.rgb * (1.0 - darkening), mask);
    ROUGHNESS = mix(roughness, wet_roughness, mask);
    METALLIC = metallic;
    SPECULAR = mix(0.5, 0.9, mask);  // glossy film sells "wet"
}
```

Assumes near-uniform mesh scale; for heavily non-uniform scale, use the inverse-transpose normal transform instead of `MODEL_MATRIX` directly.

### 2. Convert existing materials while preserving their values

This is the source's "swap in the pre-configured shader; all values retained" step. Run once (editor tool or import-time), not per frame.

```gdscript
@tool
func convert_to_wet_material(src: StandardMaterial3D, wet_shader: Shader) -> ShaderMaterial:
    var m := ShaderMaterial.new()
    m.shader = wet_shader
    m.set_shader_parameter("albedo_color", src.albedo_color)
    m.set_shader_parameter("metallic", src.metallic)
    m.set_shader_parameter("roughness", src.roughness)
    if src.albedo_texture != null:
        m.set_shader_parameter("albedo_tex", src.albedo_texture)
    return m
```

Extend the copy list for any extra `StandardMaterial3D` properties your project uses (normal maps, emission, UV transforms). Keep the converter in one place so every wet-enabled material is produced identically.

### 3. Drive wetness at runtime with instance uniforms

```gdscript
class_name WetnessController
extends Node

@export var targets: Array[GeometryInstance3D] = []
@export_range(0.0, 1.0) var wet_rate: float = 0.15  # per second while raining
@export_range(0.0, 1.0) var dry_rate: float = 0.05  # per second after rain

var raining := false
var _wet: float = 0.0

func _process(delta: float) -> void:
    var goal := 1.0 if raining else 0.0
    var rate := wet_rate if raining else dry_rate
    _wet = move_toward(_wet, goal, rate * delta)
    for gi: GeometryInstance3D in targets:
        gi.set_instance_shader_parameter("wet_amount", _wet)
```

Use `Area3D` rain zones to toggle `raining` for objects inside them. For scene-wide weather without enumerating targets, register a global shader parameter once (`RenderingServer.global_shader_parameter_add("global_wet_amount", RenderingServer.GLOBAL_VAR_TYPE_FLOAT, 0.0)`) and set it per frame with `RenderingServer.global_shader_parameter_set`; read it in the shader instead of the instance uniform.

### 4. Planned extension: heightmap-guided flow

The source author states heightmap support so wetness flows along map bumps is **planned, not shipped**. To implement it: sample a heightmap texture in the fragment stage and bias the wet mask along its gradient (e.g., offset `world_pos.xz` lookups downhill using the height texture derivatives) so streaks follow surface relief. Do not treat this as part of the evidenced technique.

## Tunables

| Parameter | Typical Value | Notes |
|---|---|---|
| `wet_amount` | 0–1, animated | Instance uniform or global shader parameter |
| `up_bias` | 0.35 | Lower = walls and slopes also wet; higher = only flat tops |
| `darkening` | 0.4–0.6 | Wet albedo multiplier = `1.0 - darkening` |
| `wet_roughness` | 0.05–0.15 | Lower = glassy puddle look |
| `noise_scale` | 4–16 | World-space breakup frequency |
| `wet_rate` / `dry_rate` | 0.15 / 0.05 per s | Asymmetric rates feel natural (wets fast, dries slow) |

## Failure Modes & Gotchas

- **View-space normal mistake:** using fragment `NORMAL` directly makes the up-facing mask rotate with the camera. Always pass world normal from `vertex()` via a varying.
- **Expecting in-place injection:** the engine cannot add wet logic to a live `StandardMaterial3D`. The value-preserving step is the one-time conversion, not runtime patching.
- **Duplicate shaders per material:** keep ONE shared `.gdshader`; per-material differences live in uniforms, per-object state in instance uniforms. Otherwise you pay shader compilation and draw-call overhead.
- **Over-darkening:** on already-dark albedos, clamp the darkened result or lower `darkening`.
- **Transparent/unshaded materials:** out of scope; this skill targets opaque PBR surfaces.
- **Skinned meshes:** verify the `MODEL_MATRIX` world-normal transform still behaves with skeletal animation before relying on the up-facing mask.

## Verification

1. With `wet_amount = 0`, converted materials render identically to their original `StandardMaterial3D` values (compare albedo/roughness response under a fixed light).
2. Raising `wet_amount` darkens and glosses upward-facing surfaces; downward-facing and underside faces stay mostly dry.
3. Rotating the camera does NOT shift the wet mask boundaries (confirms world-space normal).
4. `WetnessController` animates wet/dry transitions smoothly with no per-material edits during play.
5. Multiple meshes share one shader resource; adding wet-enabled objects does not add new shader variants.

## Confidence

`extracted` — the architecture (material-preserving wet swap, custom-function value bridge, geometry-only mask, planned heightmap flow) comes from a public post that showed the technique in another engine; no code was published. The mapping, shader, and scripts here are engineering guidance consistent with that post, **not** source-extracted implementation and **not** verified inside Summer Engine.

## Evidence

- A public X post describing the technique (demo video only, no code).

Files in this skill

  • SKILL.md10.1 KB
  • resource.yaml940 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.