From 47362036bf50be4e2e5112b388ebb7c1f14da4e5 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 6 Apr 2026 18:06:16 +0200 Subject: [PATCH] Enhance advanced rendering settings and water material handling --- src/rendering/advanced-rendering.js | 3 + src/rendering/water-material.js | 73 ++++++++++++++++-------- src/runtime-three/runtime-host.js | 84 ++++++++++++++++++++++++---- src/viewport-three/ViewportCanvas.js | 8 --- src/viewport-three/viewport-host.js | 1 + 5 files changed, 127 insertions(+), 42 deletions(-) diff --git a/src/rendering/advanced-rendering.js b/src/rendering/advanced-rendering.js index 87c37e75..db36681a 100644 --- a/src/rendering/advanced-rendering.js +++ b/src/rendering/advanced-rendering.js @@ -43,7 +43,10 @@ export function configureAdvancedRenderingRenderer(renderer, settings) { renderer.toneMappingExposure = settings.toneMapping.exposure; } export function createAdvancedRenderingComposer(renderer, scene, camera, settings) { + const requiresDepthBuffer = settings.ambientOcclusion.enabled || settings.depthOfField.enabled; const composer = new EffectComposer(renderer, { + depthBuffer: requiresDepthBuffer, + stencilBuffer: false, multisampling: 0, frameBufferType: renderer.capabilities.isWebGL2 ? HalfFloatType : UnsignedByteType }); diff --git a/src/rendering/water-material.js b/src/rendering/water-material.js index ca97e264..7f7c961d 100644 --- a/src/rendering/water-material.js +++ b/src/rendering/water-material.js @@ -95,6 +95,13 @@ export function collectWaterContactPatches(volume, contactBounds) { .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); + }); +} + export function createWaterMaterial(options) { if (options.wireframe) { return { @@ -105,7 +112,8 @@ export function createWaterMaterial(options) { opacity: Math.min(1, options.opacity + 0.2), depthWrite: false }), - animationUniform: null + animationUniform: null, + contactPatchesUniform: null }; } @@ -117,16 +125,14 @@ export function createWaterMaterial(options) { opacity: options.opacity, depthWrite: false }), - animationUniform: null + animationUniform: null, + contactPatchesUniform: null }; } const animationUniform = { value: options.time }; const halfSize = new Vector2(Math.max(options.halfSize.x, WATER_CONTACT_EPSILON), Math.max(options.halfSize.z, WATER_CONTACT_EPSILON)); - const contactPatches = Array.from({ length: MAX_WATER_CONTACT_PATCHES }, (_, index) => { - const patch = options.contactPatches?.[index]; - return new Vector4(patch?.x ?? 0, patch?.z ?? 0, patch?.radius ?? 0, patch?.intensity ?? 0); - }); + const contactPatchesUniform = { value: createWaterContactPatchUniformValue(options.contactPatches) }; 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)); @@ -211,25 +217,39 @@ export function createWaterMaterial(options) { ); } + float fbm(vec2 p) { + float value = 0.0; + float amplitude = 0.5; + + for (int octave = 0; octave < 4; octave += 1) { + value += noise(p) * amplitude; + p = p * 2.02 + vec2(17.1, 11.7); + amplitude *= 0.5; + } + + return value; + } + void main() { vec3 normal = normalize(vWaveNormal); vec3 viewDir = normalize(vViewDir); float fresnel = pow(1.0 - clamp(dot(viewDir, normal), 0.0, 1.0), 2.8); - float refractPattern = - sin((vLocalSurfaceUv.x + normal.x * 0.6) * 2.2 + time * 0.8) * - sin((vLocalSurfaceUv.y + normal.z * 0.4) * 1.9 - time * 0.65); - float detail = noise(vLocalSurfaceUv * 1.8 + vec2(time * 0.12, -time * 0.09)); - float refraction = refractPattern * 0.08 + (detail - 0.5) * 0.12; + float largeWave = fbm(vLocalSurfaceUv * 0.42 + vec2(time * 0.06, -time * 0.04)); + float mediumWave = fbm(vLocalSurfaceUv * 0.95 + normal.xz * 0.55 + vec2(-time * 0.11, time * 0.09)); + float microWave = noise(vLocalSurfaceUv * 3.6 + normal.xz * 1.6 + vec2(time * 0.24, -time * 0.19)); + float caustics = fbm(vLocalSurfaceUv * 1.8 + normal.xz * 1.2 + vec2(time * 0.16, -time * 0.14)); + caustics *= fbm(vLocalSurfaceUv * 2.7 - normal.xz * 1.4 + vec2(-time * 0.21, time * 0.18)); vec3 deepTint = waterColor * vec3(0.52, 0.66, 0.78); vec3 shallowTint = mix(waterColor, vec3(0.72, 0.9, 1.0), 0.2 + fresnel * 0.24); - vec3 color = mix(deepTint, shallowTint, 0.58 + refraction); + float contactFoam = 0.0; + float contactRipple = 0.0; + float contactSheen = 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); float edgeFoam = isTopFace > 0.5 ? 1.0 - smoothstep(0.0, edgeBand, edgeDistance) : 0.0; - float contactFoam = 0.0; if (isTopFace > 0.5) { for (int patchIndex = 0; patchIndex < ${MAX_WATER_CONTACT_PATCHES}; patchIndex += 1) { @@ -239,22 +259,30 @@ export function createWaterMaterial(options) { } float normalizedDistance = length(vLocalSurfaceUv - patchData.xy) / patchData.z; - float ring = smoothstep(0.38, 0.72, normalizedDistance) * (1.0 - smoothstep(0.88, 1.2, normalizedDistance)); - contactFoam = max(contactFoam, ring * patchData.w); + 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); + 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; + contactFoam = max(contactFoam, foamField); + contactRipple = max(contactRipple, ripple * patchData.w); + contactSheen = max(contactSheen, contactBody * patchData.w); } } - float sparkle = max(0.0, sin(vLocalSurfaceUv.x * 5.2 + time * 1.35) * sin(vLocalSurfaceUv.y * 4.4 - time * 1.08)); - float foam = clamp(max(edgeFoam * 0.42, contactFoam) * (0.45 + waveStrength * 0.75) + sparkle * 0.06, 0.0, 0.72); - vec3 specular = vec3(pow(max(0.0, dot(reflect(-viewDir, normal), normalize(vec3(0.25, 0.88, 0.35)))), 18.0)) * (0.18 + fresnel * 0.52); + 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)); + 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, vec3(0.97, 0.99, 1.0), foam); color += specular; color += vec3(0.05, 0.08, 0.12) * fresnel; + color += vec3(0.02, 0.05, 0.08) * caustics; float alpha = isTopFace > 0.5 - ? clamp(surfaceOpacity + fresnel * 0.16 + foam * 0.12, 0.32, 0.9) - : clamp(surfaceOpacity * 0.72 + refraction * 0.05, 0.16, 0.68); + ? clamp(surfaceOpacity + fresnel * 0.18 + foam * 0.16 + contactRipple * 0.08, 0.32, 0.92) + : clamp(surfaceOpacity * 0.72 + refraction * 0.08 + caustics * 0.04, 0.16, 0.7); gl_FragColor = vec4(color, alpha); } @@ -271,7 +299,7 @@ export function createWaterMaterial(options) { waveAmplitude: { value: waveAmplitude }, isTopFace: { value: topFaceFlag }, halfSize: { value: halfSize }, - contactPatches: { value: contactPatches } + contactPatches: contactPatchesUniform }, transparent: true, depthWrite: false, @@ -280,6 +308,7 @@ export function createWaterMaterial(options) { return { material, - animationUniform + animationUniform, + contactPatchesUniform }; } \ No newline at end of file diff --git a/src/runtime-three/runtime-host.js b/src/runtime-three/runtime-host.js index 4b9184fb..4a578e24 100644 --- a/src/runtime-three/runtime-host.js +++ b/src/runtime-three/runtime-host.js @@ -3,7 +3,7 @@ import { createModelInstanceRenderGroup, disposeModelInstance } from "../assets/ import { buildBoxBrushDerivedMeshData } from "../geometry/box-brush-mesh"; import { createStarterMaterialSignature, createStarterMaterialTexture } from "../materials/starter-material-textures"; import { applyAdvancedRenderingLightShadowFlags, applyAdvancedRenderingRenderableShadowFlags, configureAdvancedRenderingRenderer, createAdvancedRenderingComposer, resolveBoxVolumeRenderPaths } from "../rendering/advanced-rendering"; -import { collectWaterContactPatches, createWaterMaterial } from "../rendering/water-material"; +import { collectWaterContactPatches, createWaterContactPatchUniformValue, createWaterMaterial } from "../rendering/water-material"; import { areAdvancedRenderingSettingsEqual, cloneAdvancedRenderingSettings } from "../document/world-settings"; import { FirstPersonNavigationController } from "./first-person-navigation-controller"; import { RapierCollisionWorld } from "./rapier-collision-world"; @@ -31,6 +31,7 @@ export class RuntimeHost { volumeTime = 0; volumeAnimatedMaterials = []; volumeAnimatedUniforms = []; + runtimeWaterContactUniforms = []; localLightObjects = new Map(); modelRenderObjects = new Map(); materialTextureCache = new Map(); @@ -215,6 +216,7 @@ export class RuntimeHost { cachedTexture.texture.dispose(); } this.materialTextureCache.clear(); + this.renderer?.forceContextLoss(); this.renderer?.dispose(); this.domElement.removeEventListener("click", this.handleRuntimeClick); this.domElement.removeEventListener("pointerdown", this.handleRuntimePointerDown); @@ -362,19 +364,9 @@ export class RuntimeHost { rebuildBrushMeshes(brushes) { this.clearBrushMeshes(); const volumeRenderPaths = this.currentWorld === null ? { fog: "performance", water: "performance" } : resolveBoxVolumeRenderPaths(this.currentWorld.advancedRendering); - const colliderBounds = this.runtimeScene?.colliders.map((collider) => ({ - min: collider.worldBounds.min, - max: collider.worldBounds.max - })) ?? []; for (const brush of brushes) { const geometry = buildBoxBrushDerivedMeshData(brush).geometry; - const contactPatches = brush.volume.mode === "water" - ? collectWaterContactPatches({ - center: brush.center, - rotationDegrees: brush.rotationDegrees, - size: brush.size - }, colliderBounds) - : []; + const contactPatches = brush.volume.mode === "water" ? this.collectRuntimeWaterContactPatches(brush) : []; const materials = [ this.createFaceMaterial(brush, "posX", brush.faces.posX.material, volumeRenderPaths, contactPatches), this.createFaceMaterial(brush, "negX", brush.faces.negX.material, volumeRenderPaths, contactPatches), @@ -446,6 +438,12 @@ export class RuntimeHost { if (waterMaterial.animationUniform !== null) { this.volumeAnimatedUniforms.push(waterMaterial.animationUniform); } + if (faceId === "posY" && waterMaterial.contactPatchesUniform !== null) { + this.runtimeWaterContactUniforms.push({ + brush, + uniform: waterMaterial.contactPatchesUniform + }); + } return waterMaterial.material; } if (brush.volume.mode === "fog") { @@ -547,6 +545,65 @@ export class RuntimeHost { } this.brushMeshes.clear(); this.volumeAnimatedMaterials.length = 0; + this.volumeAnimatedUniforms.length = 0; + this.runtimeWaterContactUniforms.length = 0; + } + createPlayerWaterContactBounds() { + if (this.runtimeScene === null || this.currentFirstPersonTelemetry === null) { + return null; + } + const feetPosition = this.currentFirstPersonTelemetry.feetPosition; + const playerShape = this.runtimeScene.playerCollider; + switch (playerShape.mode) { + case "capsule": + return { + min: { + x: feetPosition.x - playerShape.radius, + y: feetPosition.y, + z: feetPosition.z - playerShape.radius + }, + max: { + x: feetPosition.x + playerShape.radius, + y: feetPosition.y + playerShape.height, + z: feetPosition.z + playerShape.radius + } + }; + case "box": + return { + min: { + x: feetPosition.x - playerShape.size.x * 0.5, + y: feetPosition.y, + z: feetPosition.z - playerShape.size.z * 0.5 + }, + max: { + x: feetPosition.x + playerShape.size.x * 0.5, + y: feetPosition.y + playerShape.size.y, + z: feetPosition.z + playerShape.size.z * 0.5 + } + }; + case "none": + return null; + } + } + collectRuntimeWaterContactPatches(brush) { + 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({ + center: brush.center, + rotationDegrees: brush.rotationDegrees, + size: brush.size + }, contactBounds); + } + updateRuntimeWaterContactUniforms() { + for (const binding of this.runtimeWaterContactUniforms) { + binding.uniform.value = createWaterContactPatchUniformValue(this.collectRuntimeWaterContactPatches(binding.brush)); + } } clearModelInstances() { for (const mixer of this.animationMixers.values()) { @@ -605,6 +662,9 @@ export class RuntimeHost { else { this.setInteractionPrompt(null); } + if (this.runtimeWaterContactUniforms.length > 0) { + this.updateRuntimeWaterContactUniforms(); + } if (this.advancedRenderingComposer !== null) { this.advancedRenderingComposer.render(dt); return; diff --git a/src/viewport-three/ViewportCanvas.js b/src/viewport-three/ViewportCanvas.js index 3138d7ee..19f33d31 100644 --- a/src/viewport-three/ViewportCanvas.js +++ b/src/viewport-three/ViewportCanvas.js @@ -16,14 +16,6 @@ export function ViewportCanvas({ panelId, world, sceneDocument, projectAssets, l if (container === null) { return; } - const testCanvas = document.createElement("canvas"); - const hasWebGl = testCanvas.getContext("webgl2") !== null || - testCanvas.getContext("webgl") !== null || - testCanvas.getContext("experimental-webgl") !== null; - if (!hasWebGl) { - setViewportMessage("WebGL is unavailable in this browser environment. The viewport shell is visible, but rendering is disabled."); - return; - } try { const viewportHost = new ViewportHost(); hostRef.current = viewportHost; diff --git a/src/viewport-three/viewport-host.js b/src/viewport-three/viewport-host.js index 72fa45c9..63ebe981 100644 --- a/src/viewport-three/viewport-host.js +++ b/src/viewport-three/viewport-host.js @@ -449,6 +449,7 @@ export class ViewportHost { this.boxCreatePreviewMesh.material.dispose(); this.boxCreatePreviewEdges.geometry.dispose(); this.boxCreatePreviewEdges.material.dispose(); + this.renderer.forceContextLoss(); this.renderer.dispose(); if (this.container !== null && this.container.contains(this.renderer.domElement)) { this.container.removeChild(this.renderer.domElement);