From 282c282f9cf6aa4939086a5249f2f916840ae386 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 7 Apr 2026 06:31:29 +0200 Subject: [PATCH] Add reflection support to water material --- src/rendering/water-material.ts | 100 ++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 18 deletions(-) diff --git a/src/rendering/water-material.ts b/src/rendering/water-material.ts index 97ac2824..e8b6b33b 100644 --- a/src/rendering/water-material.ts +++ b/src/rendering/water-material.ts @@ -1,6 +1,20 @@ -import { DoubleSide, Euler, MeshBasicMaterial, Quaternion, ShaderMaterial, Vector2, Vector3, Vector4 } from "three"; +import { + DoubleSide, + Euler, + Matrix4, + MeshBasicMaterial, + Quaternion, + ShaderMaterial, + Texture, + UniformsLib, + UniformsUtils, + Vector2, + Vector3, + Vector4 +} from "three"; import type { Vec3 } from "../core/vector"; +import { MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT } from "../document/brushes"; export interface WaterContactBounds { min: Vec3; @@ -46,6 +60,15 @@ export interface WaterMaterialResult { contactPatchesUniform: { value: Vector4[] } | null; contactPatchAxesUniform: { value: Vector2[] } | null; contactPatchShapesUniform: { value: number[] } | null; + reflectionTextureUniform: { value: Texture | null } | null; + reflectionMatrixUniform: { value: Matrix4 } | null; + reflectionEnabledUniform: { value: number } | null; +} + +interface WaterMaterialReflectionOptions { + texture: Texture | null; + enabled: boolean; + strength?: number; } interface WaterMaterialOptions { @@ -62,6 +85,7 @@ interface WaterMaterialOptions { z: number; }; contactPatches?: WaterContactPatch[]; + reflection?: WaterMaterialReflectionOptions; } interface OrientedWaterVolume { @@ -75,7 +99,7 @@ interface TriangleMeshSegmentSample { normal: Vector3; } -const MAX_WATER_CONTACT_PATCHES = 6; +const MAX_WATER_CONTACT_PATCHES = MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT; const WATER_CONTACT_EPSILON = 1e-4; function createBoundsCorners(bounds: WaterContactBounds) { @@ -759,7 +783,7 @@ function appendTriangleMeshContactPatches( patches.push(...mergeTriangleMeshContactPatches(rawPatches, bandMinimumThickness, source.mergeProfile)); } -export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[]): WaterContactPatch[] { +export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[], patchLimit = MAX_WATER_CONTACT_PATCHES): WaterContactPatch[] { const inverseRotation = createInverseVolumeRotation(volume.rotationDegrees); const halfX = Math.max(volume.size.x * 0.5, WATER_CONTACT_EPSILON); const halfY = Math.max(volume.size.y * 0.5, WATER_CONTACT_EPSILON); @@ -851,9 +875,11 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB } } + const clampedPatchLimit = Math.max(1, Math.min(patchLimit, MAX_WATER_CONTACT_PATCHES)); + return patches .sort((left, right) => right.halfWidth * right.halfDepth - left.halfWidth * left.halfDepth) - .slice(0, MAX_WATER_CONTACT_PATCHES); + .slice(0, clampedPatchLimit); } export function createWaterContactPatchUniformValue(contactPatches?: WaterContactPatch[]): Vector4[] { @@ -890,7 +916,10 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria animationUniform: null, contactPatchesUniform: null, contactPatchAxesUniform: null, - contactPatchShapesUniform: null + contactPatchShapesUniform: null, + reflectionTextureUniform: null, + reflectionMatrixUniform: null, + reflectionEnabledUniform: null }; } @@ -905,7 +934,10 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria animationUniform: null, contactPatchesUniform: null, contactPatchAxesUniform: null, - contactPatchShapesUniform: null + contactPatchShapesUniform: null, + reflectionTextureUniform: null, + reflectionMatrixUniform: null, + reflectionEnabledUniform: null }; } @@ -914,8 +946,12 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria const contactPatchesUniform = { value: createWaterContactPatchUniformValue(options.contactPatches) }; const contactPatchAxesUniform = { value: createWaterContactPatchAxisUniformValue(options.contactPatches) }; const contactPatchShapesUniform = { value: createWaterContactPatchShapeUniformValue(options.contactPatches) }; + const reflectionTextureUniform = { value: options.reflection?.texture ?? null }; + const reflectionMatrixUniform = { value: new Matrix4() }; + const reflectionEnabledUniform = { + value: options.reflection?.enabled === true ? Math.max(0, Math.min(1, options.reflection?.strength ?? 0.36)) : 0 + }; const waveStrength = Math.max(0, options.waveStrength); - const waveAmplitude = 0.016 + Math.min(0.12, waveStrength * 0.06); const clampedOpacity = Math.max(0.14, Math.min(1, options.opacity)); const topFaceFlag = options.isTopFace ? 1 : 0; const hex = options.colorHex.replace("#", ""); @@ -926,19 +962,21 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria const vertexShader = /* glsl */ ` uniform float time; uniform float waveStrength; - uniform float waveAmplitude; uniform float isTopFace; + uniform mat4 reflectionMatrix; varying vec2 vLocalSurfaceUv; varying vec3 vWaveNormal; varying vec3 vWorldPos; varying vec3 vViewDir; + varying vec4 vReflectionCoord; #include void main() { vec3 transformedPosition = position; vLocalSurfaceUv = position.xz; vWaveNormal = vec3(0.0, 1.0, 0.0); + vReflectionCoord = vec4(0.0); if (isTopFace > 0.5) { vec2 dirA = normalize(vec2(0.92, 0.38)); @@ -951,8 +989,6 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria float waveB = sin(phaseB) * 0.30; float waveC = sin(phaseC) * 0.15; - transformedPosition.y += (waveA + waveB + waveC) * waveAmplitude; - vec2 slope = dirA * (cos(phaseA) / 2.3) * 0.55 + dirB * (cos(phaseB) / 1.45) * 0.30 + @@ -964,6 +1000,7 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria vec4 mvPosition = viewMatrix * worldPos; vWorldPos = worldPos.xyz; vViewDir = normalize(cameraPosition - worldPos.xyz); + vReflectionCoord = reflectionMatrix * worldPos; gl_Position = projectionMatrix * mvPosition; #include } @@ -981,11 +1018,14 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria uniform vec4 contactPatches[${MAX_WATER_CONTACT_PATCHES}]; uniform vec2 contactPatchAxes[${MAX_WATER_CONTACT_PATCHES}]; uniform float contactPatchShapes[${MAX_WATER_CONTACT_PATCHES}]; + uniform sampler2D reflectionTexture; + uniform float reflectionEnabled; varying vec2 vLocalSurfaceUv; varying vec3 vWaveNormal; varying vec3 vWorldPos; varying vec3 vViewDir; + varying vec4 vReflectionCoord; #include float hash(vec2 p) { @@ -1049,6 +1089,8 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria float contactFoam = 0.0; float contactRipple = 0.0; float contactSheen = 0.0; + float reflectionMask = 0.0; + vec3 reflectionColor = vec3(0.0); float edgeDistance = min(halfSize.x - abs(vLocalSurfaceUv.x), halfSize.y - abs(vLocalSurfaceUv.y)); float edgeBand = max(0.22, min(halfSize.x, halfSize.y) * 0.12); @@ -1099,9 +1141,21 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria float refraction = (largeWave - 0.5) * 0.18 + (mediumWave - 0.5) * 0.14 + (microWave - 0.5) * 0.08 + contactRipple * 0.06; float glints = smoothstep(0.78, 0.97, fbm(vLocalSurfaceUv * 4.8 + normal.xz * 2.2 + vec2(time * 0.38, -time * 0.31))) * (0.14 + fresnel * 0.28); vec3 color = mix(deepTint, shallowTint, clamp(0.46 + refraction + fresnel * 0.24 + caustics * 0.08, 0.05, 0.98)); + + if (isTopFace > 0.5 && reflectionEnabled > 0.0 && vReflectionCoord.w > 0.0) { + vec2 reflectionUv = vReflectionCoord.xy / vReflectionCoord.w; + reflectionUv += normal.xz * (0.01 + waveStrength * 0.012) + vec2((microWave - 0.5) * 0.018, (mediumWave - 0.5) * 0.015); + if (reflectionUv.x >= 0.0 && reflectionUv.x <= 1.0 && reflectionUv.y >= 0.0 && reflectionUv.y <= 1.0) { + reflectionColor = texture2D(reflectionTexture, clamp(reflectionUv, vec2(0.001), vec2(0.999))).rgb; + reflectionColor = mix(reflectionColor, shallowTint, 0.16); + reflectionMask = reflectionEnabled * clamp(0.12 + fresnel * 0.92 + glints * 0.28, 0.0, 0.92); + } + } + float foam = clamp(max(edgeFoam * 0.48, contactFoam) * (0.52 + waveStrength * 0.8) + caustics * 0.08 + glints * 0.06, 0.0, 0.84); vec3 specular = vec3(pow(max(0.0, dot(reflect(-viewDir, normal), normalize(vec3(0.25, 0.88, 0.35)))), 18.0)) * (0.14 + fresnel * 0.56 + caustics * 0.14 + contactSheen * 0.12); + color = mix(color, reflectionColor, reflectionMask); color = mix(color, vec3(0.97, 0.99, 1.0), foam); color += specular; color += vec3(0.05, 0.08, 0.12) * fresnel; @@ -1116,21 +1170,28 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria } `; - const material = new ShaderMaterial({ - vertexShader, - fragmentShader, - uniforms: { + const uniforms = UniformsUtils.merge([ + UniformsLib.fog, + { time: animationUniform, waterColor: { value: [cr, cg, cb] }, surfaceOpacity: { value: clampedOpacity }, waveStrength: { value: waveStrength }, - waveAmplitude: { value: waveAmplitude }, isTopFace: { value: topFaceFlag }, halfSize: { value: halfSize }, contactPatches: contactPatchesUniform, contactPatchAxes: contactPatchAxesUniform, - contactPatchShapes: contactPatchShapesUniform - }, + contactPatchShapes: contactPatchShapesUniform, + reflectionTexture: reflectionTextureUniform, + reflectionMatrix: reflectionMatrixUniform, + reflectionEnabled: reflectionEnabledUniform + } + ]); + + const material = new ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms, transparent: true, depthWrite: false, fog: true, @@ -1142,6 +1203,9 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria animationUniform, contactPatchesUniform, contactPatchAxesUniform, - contactPatchShapesUniform + contactPatchShapesUniform, + reflectionTextureUniform, + reflectionMatrixUniform, + reflectionEnabledUniform }; } \ No newline at end of file