diff --git a/src/rendering/water-material.js b/src/rendering/water-material.js index 7f7c961d..1088e2f1 100644 --- a/src/rendering/water-material.js +++ b/src/rendering/water-material.js @@ -74,31 +74,29 @@ export function collectWaterContactPatches(volume, contactBounds) { continue; } - const radius = Math.max(0.2, Math.min(Math.max(overlapWidth, overlapDepth) * 0.55, Math.min(halfX, halfZ) * 0.85)); const verticalDistance = Math.min(Math.abs(surfaceY - minY), Math.abs(maxY - surfaceY)); - const intensity = 1 - Math.min(verticalDistance / surfaceBand, 1); - if (intensity <= WATER_CONTACT_EPSILON) { + if (1 - Math.min(verticalDistance / surfaceBand, 1) <= WATER_CONTACT_EPSILON) { continue; } patches.push({ x: (overlapMinX + overlapMaxX) * 0.5, z: (overlapMinZ + overlapMaxZ) * 0.5, - radius, - intensity: 0.45 + intensity * 0.55 + halfWidth: overlapWidth * 0.5, + halfDepth: overlapDepth * 0.5 }); } return patches - .sort((left, right) => right.radius * right.intensity - left.radius * left.intensity) + .sort((left, right) => right.halfWidth * right.halfDepth - left.halfWidth * left.halfDepth) .slice(0, MAX_WATER_CONTACT_PATCHES); } export function createWaterContactPatchUniformValue(contactPatches) { return Array.from({ length: MAX_WATER_CONTACT_PATCHES }, (_, index) => { const patch = contactPatches?.[index]; - return new Vector4(patch?.x ?? 0, patch?.z ?? 0, patch?.radius ?? 0, patch?.intensity ?? 0); + return new Vector4(patch?.x ?? 0, patch?.z ?? 0, patch?.halfWidth ?? 0, patch?.halfDepth ?? 0); }); } @@ -254,18 +252,24 @@ export function createWaterMaterial(options) { if (isTopFace > 0.5) { for (int patchIndex = 0; patchIndex < ${MAX_WATER_CONTACT_PATCHES}; patchIndex += 1) { vec4 patchData = contactPatches[patchIndex]; - if (patchData.z <= 0.0) { + if (patchData.z <= 0.0 || patchData.w <= 0.0) { continue; } - float normalizedDistance = length(vLocalSurfaceUv - patchData.xy) / patchData.z; - float contactBody = 1.0 - smoothstep(0.0, 0.82, normalizedDistance); - float ripple = (sin(normalizedDistance * 14.0 - time * (2.4 + patchData.w * 1.6)) * 0.5 + 0.5) * exp(-normalizedDistance * 3.2); + vec2 regionDelta = abs(vLocalSurfaceUv - patchData.xy) - patchData.zw; + vec2 outsideDelta = max(regionDelta, 0.0); + float outsideDistance = length(outsideDelta); + float insideDistance = min(max(regionDelta.x, regionDelta.y), 0.0); + float signedDistance = outsideDistance + insideDistance; + float boundaryScale = max(min(patchData.z, patchData.w), 0.18); + float normalizedDistance = abs(signedDistance) / boundaryScale; + float contactBody = 1.0 - smoothstep(0.0, 0.65, max(signedDistance, 0.0) / boundaryScale); + float ripple = (sin(normalizedDistance * 13.0 - time * 3.2) * 0.5 + 0.5) * exp(-normalizedDistance * 2.6); float wakeNoise = noise(vLocalSurfaceUv * 3.4 + vec2(time * 0.34, -time * 0.28)); - float foamField = max(contactBody * 0.38, ripple * (0.72 + wakeNoise * 0.28)) * patchData.w; + float foamField = max(contactBody * 0.42, ripple * (0.72 + wakeNoise * 0.28)); contactFoam = max(contactFoam, foamField); - contactRipple = max(contactRipple, ripple * patchData.w); - contactSheen = max(contactSheen, contactBody * patchData.w); + contactRipple = max(contactRipple, ripple); + contactSheen = max(contactSheen, contactBody); } } diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index b4855328..10bea91d 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -71,6 +71,7 @@ interface LocalLightRenderObjects { interface RuntimeWaterContactUniformBinding { brush: RuntimeBoxBrushInstance; uniform: { value: import("three").Vector4[] }; + staticContactPatches: ReturnType; } const FALLBACK_FACE_COLOR = 0x747d89; @@ -132,7 +133,7 @@ export class RuntimeHost { this.scene.add(this.localLightGroup); this.scene.add(this.brushGroup); this.scene.add(this.modelGroup); - this.renderer = enableRendering ? new WebGLRenderer({ antialias: true, alpha: true }) : null; + this.renderer = enableRendering ? new WebGLRenderer({ antialias: false, alpha: true }) : null; this.domElement = this.renderer?.domElement ?? document.createElement("canvas"); if (this.renderer !== null) { @@ -530,7 +531,8 @@ export class RuntimeHost { for (const brush of brushes) { const geometry = buildBoxBrushDerivedMeshData(brush).geometry; - const contactPatches = brush.volume.mode === "water" ? this.collectRuntimeWaterContactPatches(brush) : []; + const staticContactPatches = brush.volume.mode === "water" ? this.collectRuntimeStaticWaterContactPatches(brush) : []; + const contactPatches = brush.volume.mode === "water" ? this.mergeRuntimeWaterContactPatches(staticContactPatches, this.collectRuntimePlayerWaterContactPatches(brush)) : []; const materials = [ this.createFaceMaterial(brush, "posX", brush.faces.posX.material, volumeRenderPaths, contactPatches), @@ -631,7 +633,8 @@ export class RuntimeHost { if (faceId === "posY" && waterMaterial.contactPatchesUniform !== null) { this.runtimeWaterContactUniforms.push({ brush, - uniform: waterMaterial.contactPatchesUniform + uniform: waterMaterial.contactPatchesUniform, + staticContactPatches }); } @@ -806,16 +809,11 @@ export class RuntimeHost { } } - private collectRuntimeWaterContactPatches(brush: RuntimeBoxBrushInstance) { + private collectRuntimeStaticWaterContactPatches(brush: RuntimeBoxBrushInstance) { const contactBounds = this.runtimeScene?.colliders.map((collider) => ({ min: collider.worldBounds.min, max: collider.worldBounds.max })) ?? []; - const playerBounds = this.createPlayerWaterContactBounds(); - - if (playerBounds !== null) { - contactBounds.push(playerBounds); - } return collectWaterContactPatches( { @@ -827,9 +825,35 @@ export class RuntimeHost { ); } + private collectRuntimePlayerWaterContactPatches(brush: RuntimeBoxBrushInstance) { + const playerBounds = this.createPlayerWaterContactBounds(); + + if (playerBounds === null) { + return []; + } + + return collectWaterContactPatches( + { + center: brush.center, + rotationDegrees: brush.rotationDegrees, + size: brush.size + }, + [playerBounds] + ); + } + + private mergeRuntimeWaterContactPatches( + staticContactPatches: ReturnType, + dynamicContactPatches: ReturnType + ) { + return [...dynamicContactPatches, ...staticContactPatches].slice(0, 6); + } + private updateRuntimeWaterContactUniforms() { for (const binding of this.runtimeWaterContactUniforms) { - binding.uniform.value = createWaterContactPatchUniformValue(this.collectRuntimeWaterContactPatches(binding.brush)); + binding.uniform.value = createWaterContactPatchUniformValue( + this.mergeRuntimeWaterContactPatches(binding.staticContactPatches, this.collectRuntimePlayerWaterContactPatches(binding.brush)) + ); } } diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index 591d8d07..b3089ba5 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -262,7 +262,7 @@ export class ViewportHost { private readonly scene = new Scene(); private readonly perspectiveCamera = new PerspectiveCamera(60, 1, 0.1, 1000); private readonly orthographicCamera = new OrthographicCamera(-10, 10, 10, -10, 0.1, 1000); - private readonly renderer = new WebGLRenderer({ antialias: true, alpha: true }); + private readonly renderer = new WebGLRenderer({ antialias: false, alpha: true }); private readonly cameraTarget = new Vector3(0, 0, 0); private readonly cameraOffset = new Vector3(); private readonly cameraForward = new Vector3();