From de430fdda16df4c3491eabbc5c947d9ce8b21007 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 7 Apr 2026 05:00:49 +0200 Subject: [PATCH] Add support for oriented boxes in water material rendering --- src/rendering/water-material.js | 111 ++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 11 deletions(-) diff --git a/src/rendering/water-material.js b/src/rendering/water-material.js index 1088e2f1..d8dc236b 100644 --- a/src/rendering/water-material.js +++ b/src/rendering/water-material.js @@ -16,6 +16,25 @@ function createBoundsCorners(bounds) { ]; } +function createOrientedBoxCorners(box) { + const halfSize = { + x: box.size.x * 0.5, + y: box.size.y * 0.5, + z: box.size.z * 0.5 + }; + const rotation = new Quaternion().setFromEuler(new Euler((box.rotationDegrees.x * Math.PI) / 180, (box.rotationDegrees.y * Math.PI) / 180, (box.rotationDegrees.z * Math.PI) / 180, "XYZ")); + return [ + new Vector3(-halfSize.x, -halfSize.y, -halfSize.z), + new Vector3(-halfSize.x, -halfSize.y, halfSize.z), + new Vector3(-halfSize.x, halfSize.y, -halfSize.z), + new Vector3(-halfSize.x, halfSize.y, halfSize.z), + new Vector3(halfSize.x, -halfSize.y, -halfSize.z), + new Vector3(halfSize.x, -halfSize.y, halfSize.z), + new Vector3(halfSize.x, halfSize.y, -halfSize.z), + new Vector3(halfSize.x, halfSize.y, halfSize.z) + ].map((corner) => corner.applyQuaternion(rotation).add(new Vector3(box.center.x, box.center.y, box.center.z))); +} + function createInverseVolumeRotation(rotationDegrees) { return new Quaternion() .setFromEuler(new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ")) @@ -32,8 +51,8 @@ export function collectWaterContactPatches(volume, contactBounds) { const localPoint = new Vector3(); const patches = []; - for (const bounds of contactBounds) { - const corners = createBoundsCorners(bounds); + for (const source of contactBounds) { + const corners = "kind" in source ? createOrientedBoxCorners(source) : createBoundsCorners(source); let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; @@ -80,11 +99,64 @@ export function collectWaterContactPatches(volume, contactBounds) { continue; } + let axisX = 1; + let axisZ = 0; + let halfWidth = overlapWidth * 0.5; + let halfDepth = overlapDepth * 0.5; + let centerX = (overlapMinX + overlapMaxX) * 0.5; + let centerZ = (overlapMinZ + overlapMaxZ) * 0.5; + + if ("kind" in source) { + const sourceRotation = new Quaternion().setFromEuler(new Euler((source.rotationDegrees.x * Math.PI) / 180, (source.rotationDegrees.y * Math.PI) / 180, (source.rotationDegrees.z * Math.PI) / 180, "XYZ")); + const projectedSourceX = new Vector2(1, 0).set(new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x, new Vector3(1, 0, 0).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z); + const projectedSourceZ = new Vector2(1, 0).set(new Vector3(0, 0, 1).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).x, new Vector3(0, 0, 1).applyQuaternion(sourceRotation).applyQuaternion(inverseRotation).z); + const primaryAxis = projectedSourceX.lengthSq() >= projectedSourceZ.lengthSq() ? projectedSourceX : projectedSourceZ; + + if (primaryAxis.lengthSq() > WATER_CONTACT_EPSILON) { + primaryAxis.normalize(); + const secondaryAxis = new Vector2(-primaryAxis.y, primaryAxis.x); + if (projectedSourceZ.lengthSq() > WATER_CONTACT_EPSILON && projectedSourceZ.clone().normalize().dot(secondaryAxis) < 0) { + secondaryAxis.negate(); + } + + let minPrimary = Number.POSITIVE_INFINITY; + let maxPrimary = Number.NEGATIVE_INFINITY; + let minSecondary = Number.POSITIVE_INFINITY; + let maxSecondary = Number.NEGATIVE_INFINITY; + + for (const corner of corners) { + localPoint.copy(corner); + localPoint.x -= volume.center.x; + localPoint.y -= volume.center.y; + localPoint.z -= volume.center.z; + localPoint.applyQuaternion(inverseRotation); + const projectedPoint = new Vector2(localPoint.x, localPoint.z); + const primaryDistance = projectedPoint.dot(primaryAxis); + const secondaryDistance = projectedPoint.dot(secondaryAxis); + minPrimary = Math.min(minPrimary, primaryDistance); + maxPrimary = Math.max(maxPrimary, primaryDistance); + minSecondary = Math.min(minSecondary, secondaryDistance); + maxSecondary = Math.max(maxSecondary, secondaryDistance); + } + + const patchCenterPrimary = (minPrimary + maxPrimary) * 0.5; + const patchCenterSecondary = (minSecondary + maxSecondary) * 0.5; + centerX = primaryAxis.x * patchCenterPrimary + secondaryAxis.x * patchCenterSecondary; + centerZ = primaryAxis.y * patchCenterPrimary + secondaryAxis.y * patchCenterSecondary; + halfWidth = (maxPrimary - minPrimary) * 0.5; + halfDepth = (maxSecondary - minSecondary) * 0.5; + axisX = primaryAxis.x; + axisZ = primaryAxis.y; + } + } + patches.push({ - x: (overlapMinX + overlapMaxX) * 0.5, - z: (overlapMinZ + overlapMaxZ) * 0.5, - halfWidth: overlapWidth * 0.5, - halfDepth: overlapDepth * 0.5 + x: centerX, + z: centerZ, + halfWidth, + halfDepth, + axisX, + axisZ }); } @@ -100,6 +172,13 @@ export function createWaterContactPatchUniformValue(contactPatches) { }); } +export function createWaterContactPatchAxisUniformValue(contactPatches) { + return Array.from({ length: MAX_WATER_CONTACT_PATCHES }, (_, index) => { + const patch = contactPatches?.[index]; + return new Vector2(patch?.axisX ?? 1, patch?.axisZ ?? 0); + }); +} + export function createWaterMaterial(options) { if (options.wireframe) { return { @@ -111,7 +190,8 @@ export function createWaterMaterial(options) { depthWrite: false }), animationUniform: null, - contactPatchesUniform: null + contactPatchesUniform: null, + contactPatchAxesUniform: null }; } @@ -124,13 +204,15 @@ export function createWaterMaterial(options) { depthWrite: false }), animationUniform: null, - contactPatchesUniform: null + contactPatchesUniform: null, + contactPatchAxesUniform: 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 contactPatchesUniform = { value: createWaterContactPatchUniformValue(options.contactPatches) }; + const contactPatchAxesUniform = { value: createWaterContactPatchAxisUniformValue(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)); @@ -193,6 +275,7 @@ export function createWaterMaterial(options) { uniform float isTopFace; uniform vec2 halfSize; uniform vec4 contactPatches[${MAX_WATER_CONTACT_PATCHES}]; + uniform vec2 contactPatchAxes[${MAX_WATER_CONTACT_PATCHES}]; varying vec2 vLocalSurfaceUv; varying vec3 vWaveNormal; @@ -256,7 +339,11 @@ export function createWaterMaterial(options) { continue; } - vec2 regionDelta = abs(vLocalSurfaceUv - patchData.xy) - patchData.zw; + vec2 patchAxis = normalize(contactPatchAxes[patchIndex]); + vec2 patchPerpendicular = vec2(-patchAxis.y, patchAxis.x); + vec2 patchDelta = vLocalSurfaceUv - patchData.xy; + vec2 orientedDelta = vec2(dot(patchDelta, patchAxis), dot(patchDelta, patchPerpendicular)); + vec2 regionDelta = abs(orientedDelta) - patchData.zw; vec2 outsideDelta = max(regionDelta, 0.0); float outsideDistance = length(outsideDelta); float insideDistance = min(max(regionDelta.x, regionDelta.y), 0.0); @@ -303,7 +390,8 @@ export function createWaterMaterial(options) { waveAmplitude: { value: waveAmplitude }, isTopFace: { value: topFaceFlag }, halfSize: { value: halfSize }, - contactPatches: contactPatchesUniform + contactPatches: contactPatchesUniform, + contactPatchAxes: contactPatchAxesUniform }, transparent: true, depthWrite: false, @@ -313,6 +401,7 @@ export function createWaterMaterial(options) { return { material, animationUniform, - contactPatchesUniform + contactPatchesUniform, + contactPatchAxesUniform }; } \ No newline at end of file