Add support for oriented boxes in water material rendering

This commit is contained in:
2026-04-07 05:00:49 +02:00
parent 0e4bd01e69
commit de430fdda1

View File

@@ -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
};
}