Refactor water material and runtime handling for better performance and accuracy
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ interface LocalLightRenderObjects {
|
||||
interface RuntimeWaterContactUniformBinding {
|
||||
brush: RuntimeBoxBrushInstance;
|
||||
uniform: { value: import("three").Vector4[] };
|
||||
staticContactPatches: ReturnType<typeof collectWaterContactPatches>;
|
||||
}
|
||||
|
||||
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<typeof collectWaterContactPatches>,
|
||||
dynamicContactPatches: ReturnType<typeof collectWaterContactPatches>
|
||||
) {
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user