Add water surface subdivision and UV interpolation in box brush mesh

This commit is contained in:
2026-04-07 07:45:15 +02:00
parent 27071fc3f3
commit de8ceafb7f
7 changed files with 198 additions and 24 deletions

View File

@@ -23,6 +23,7 @@ const EDGE_VERTEX_IDS = {
edgeZ_negX_posY: ["negX_posY_negZ", "negX_posY_posZ"],
edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"]
};
const WATER_TOP_FACE_RENDER_SEGMENTS = 12;
function cloneVec3(vector) {
return { x: vector.x, y: vector.y, z: vector.z };
}
@@ -222,6 +223,29 @@ function computeFaceBounds(vertices) {
}
return { min, max };
}
function lerpNumber(start, end, amount) {
return start + (end - start) * amount;
}
function lerpVec3(start, end, amount) {
return {
x: lerpNumber(start.x, end.x, amount),
y: lerpNumber(start.y, end.y, amount),
z: lerpNumber(start.z, end.z, amount)
};
}
function interpolateQuadSurfaceVertex(corners, u, v) {
const topEdge = lerpVec3(corners[0], corners[1], u);
const bottomEdge = lerpVec3(corners[3], corners[2], u);
return lerpVec3(topEdge, bottomEdge, v);
}
function pushRenderedFaceVertex(positions, normals, uvs, indices, vertex, normal, faceId, faceBounds, uvSize, uvState) {
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds);
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, uvState);
positions.push(vertex.x, vertex.y, vertex.z);
normals.push(normal.x, normal.y, normal.z);
uvs.push(transformedUv.x, transformedUv.y);
indices.push(indices.length);
}
export function getBoxBrushFaceVertexIds(faceId) {
return FACE_VERTEX_IDS[faceId];
}
@@ -257,6 +281,7 @@ export function buildBoxBrushDerivedMeshData(brush) {
const normal = computeNewellNormal(faceVertices);
const faceBounds = computeFaceBounds(faceVertices);
const uvSize = getFaceUvSize(faceId, faceBounds);
const uvState = brush.faces[faceId].uv;
const indexStart = indices.length;
faceSurfaces.push({
faceId,
@@ -264,15 +289,32 @@ export function buildBoxBrushDerivedMeshData(brush) {
triangles,
normal
});
for (const triangle of triangles) {
for (const vertexOffset of triangle) {
const vertex = faceVertices[vertexOffset];
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds);
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, brush.faces[faceId].uv);
positions.push(vertex.x, vertex.y, vertex.z);
normals.push(normal.x, normal.y, normal.z);
uvs.push(transformedUv.x, transformedUv.y);
indices.push(indices.length);
const useSubdividedWaterTopFace = brush.volume.mode === "water" && faceId === "posY" && brush.volume.water.surfaceDisplacementEnabled;
if (useSubdividedWaterTopFace) {
const faceCorners = faceVertices;
for (let row = 0; row < WATER_TOP_FACE_RENDER_SEGMENTS; row += 1) {
const v0 = row / WATER_TOP_FACE_RENDER_SEGMENTS;
const v1 = (row + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
for (let column = 0; column < WATER_TOP_FACE_RENDER_SEGMENTS; column += 1) {
const u0 = column / WATER_TOP_FACE_RENDER_SEGMENTS;
const u1 = (column + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
const quadVertices = [
interpolateQuadSurfaceVertex(faceCorners, u0, v0),
interpolateQuadSurfaceVertex(faceCorners, u1, v0),
interpolateQuadSurfaceVertex(faceCorners, u1, v1),
interpolateQuadSurfaceVertex(faceCorners, u0, v1)
];
for (const vertex of [quadVertices[0], quadVertices[1], quadVertices[2], quadVertices[0], quadVertices[2], quadVertices[3]]) {
pushRenderedFaceVertex(positions, normals, uvs, indices, vertex, normal, faceId, faceBounds, uvSize, uvState);
}
}
}
}
else {
for (const triangle of triangles) {
for (const vertexOffset of triangle) {
pushRenderedFaceVertex(positions, normals, uvs, indices, faceVertices[vertexOffset], normal, faceId, faceBounds, uvSize, uvState);
}
}
}
groups.push({

View File

@@ -36,6 +36,8 @@ const EDGE_VERTEX_IDS: Record<BoxEdgeId, readonly [BoxVertexId, BoxVertexId]> =
edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"]
};
const WATER_TOP_FACE_RENDER_SEGMENTS = 12;
export interface BoxBrushGeometryDiagnostic {
code: string;
message: string;
@@ -280,6 +282,50 @@ function computeFaceBounds(vertices: Vec3[]): { min: Vec3; max: Vec3 } {
return { min, max };
}
function lerpNumber(start: number, end: number, amount: number) {
return start + (end - start) * amount;
}
function lerpVec3(start: Vec3, end: Vec3, amount: number): Vec3 {
return {
x: lerpNumber(start.x, end.x, amount),
y: lerpNumber(start.y, end.y, amount),
z: lerpNumber(start.z, end.z, amount)
};
}
function interpolateQuadSurfaceVertex(
corners: readonly [Vec3, Vec3, Vec3, Vec3],
u: number,
v: number
): Vec3 {
const topEdge = lerpVec3(corners[0], corners[1], u);
const bottomEdge = lerpVec3(corners[3], corners[2], u);
return lerpVec3(topEdge, bottomEdge, v);
}
function pushRenderedFaceVertex(
positions: number[],
normals: number[],
uvs: number[],
indices: number[],
vertex: Vec3,
normal: Vec3,
faceId: BoxFaceId,
faceBounds: { min: Vec3; max: Vec3 },
uvSize: Vec2,
uvState: FaceUvState
) {
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds);
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, uvState);
positions.push(vertex.x, vertex.y, vertex.z);
normals.push(normal.x, normal.y, normal.z);
uvs.push(transformedUv.x, transformedUv.y);
indices.push(indices.length);
}
export function getBoxBrushFaceVertexIds(faceId: BoxFaceId): readonly [BoxVertexId, BoxVertexId, BoxVertexId, BoxVertexId] {
return FACE_VERTEX_IDS[faceId];
}
@@ -322,6 +368,7 @@ export function buildBoxBrushDerivedMeshData(brush: BoxBrush): DerivedBoxBrushMe
const normal = computeNewellNormal(faceVertices);
const faceBounds = computeFaceBounds(faceVertices);
const uvSize = getFaceUvSize(faceId, faceBounds);
const uvState = brush.faces[faceId].uv as FaceUvState;
const indexStart = indices.length;
faceSurfaces.push({
@@ -331,15 +378,49 @@ export function buildBoxBrushDerivedMeshData(brush: BoxBrush): DerivedBoxBrushMe
normal
});
for (const triangle of triangles) {
for (const vertexOffset of triangle) {
const vertex = faceVertices[vertexOffset];
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds);
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, brush.faces[faceId].uv as FaceUvState);
positions.push(vertex.x, vertex.y, vertex.z);
normals.push(normal.x, normal.y, normal.z);
uvs.push(transformedUv.x, transformedUv.y);
indices.push(indices.length);
const useSubdividedWaterTopFace =
brush.volume.mode === "water" &&
faceId === "posY" &&
brush.volume.water.surfaceDisplacementEnabled;
if (useSubdividedWaterTopFace) {
const faceCorners = faceVertices as [Vec3, Vec3, Vec3, Vec3];
for (let row = 0; row < WATER_TOP_FACE_RENDER_SEGMENTS; row += 1) {
const v0 = row / WATER_TOP_FACE_RENDER_SEGMENTS;
const v1 = (row + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
for (let column = 0; column < WATER_TOP_FACE_RENDER_SEGMENTS; column += 1) {
const u0 = column / WATER_TOP_FACE_RENDER_SEGMENTS;
const u1 = (column + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
const quadVertices: [Vec3, Vec3, Vec3, Vec3] = [
interpolateQuadSurfaceVertex(faceCorners, u0, v0),
interpolateQuadSurfaceVertex(faceCorners, u1, v0),
interpolateQuadSurfaceVertex(faceCorners, u1, v1),
interpolateQuadSurfaceVertex(faceCorners, u0, v1)
];
for (const vertex of [quadVertices[0], quadVertices[1], quadVertices[2], quadVertices[0], quadVertices[2], quadVertices[3]]) {
pushRenderedFaceVertex(positions, normals, uvs, indices, vertex, normal, faceId, faceBounds, uvSize, uvState);
}
}
}
} else {
for (const triangle of triangles) {
for (const vertexOffset of triangle) {
pushRenderedFaceVertex(
positions,
normals,
uvs,
indices,
faceVertices[vertexOffset],
normal,
faceId,
faceBounds,
uvSize,
uvState
);
}
}
}

View File

@@ -718,7 +718,6 @@ export function createWaterMaterial(options) {
varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir;
varying vec4 vReflectionCoord;
#include <fog_pars_vertex>
@@ -754,7 +753,6 @@ export function createWaterMaterial(options) {
vec4 worldPos = modelMatrix * vec4(transformedPosition, 1.0);
vec4 mvPosition = viewMatrix * worldPos;
vWorldPos = worldPos.xyz;
vViewDir = normalize(cameraPosition - worldPos.xyz);
vReflectionCoord = reflectionMatrix * worldPos;
gl_Position = projectionMatrix * mvPosition;
@@ -779,7 +777,6 @@ export function createWaterMaterial(options) {
varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir;
varying vec4 vReflectionCoord;
#include <fog_pars_fragment>

View File

@@ -975,7 +975,6 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir;
varying vec4 vReflectionCoord;
#include <fog_pars_vertex>
@@ -1011,7 +1010,6 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
vec4 worldPos = modelMatrix * vec4(transformedPosition, 1.0);
vec4 mvPosition = viewMatrix * worldPos;
vWorldPos = worldPos.xyz;
vViewDir = normalize(cameraPosition - worldPos.xyz);
vReflectionCoord = reflectionMatrix * worldPos;
gl_Position = projectionMatrix * mvPosition;
@@ -1036,7 +1034,6 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir;
varying vec4 vReflectionCoord;
#include <fog_pars_fragment>

View File

@@ -628,8 +628,19 @@ export class RuntimeHost {
const previousAutoClear = this.renderer.autoClear;
const previousRenderTarget = this.renderer.getRenderTarget();
const previousFogDensity = this.underwaterSceneFog.density;
const previousReflectionStates = this.runtimeWaterContactUniforms.map((waterBinding) => ({
binding: waterBinding,
enabled: waterBinding.reflectionEnabledUniform?.value ?? 0,
texture: waterBinding.reflectionTextureUniform?.value ?? null
}));
try {
this.underwaterSceneFog.density = 0;
for (const state of previousReflectionStates) {
if (state.binding.reflectionEnabledUniform !== null) {
state.binding.reflectionEnabledUniform.value = 0;
}
}
binding.reflectionTextureUniform.value = null;
this.renderer.autoClear = true;
this.renderer.setRenderTarget(binding.reflectionRenderTarget);
this.renderer.clear();
@@ -640,6 +651,14 @@ export class RuntimeHost {
this.renderer.autoClear = previousAutoClear;
this.modelGroup.visible = previousModelGroupVisibility;
this.underwaterSceneFog.density = previousFogDensity;
for (const state of previousReflectionStates) {
if (state.binding.reflectionEnabledUniform !== null) {
state.binding.reflectionEnabledUniform.value = state.enabled;
}
if (state.binding.reflectionTextureUniform !== null) {
state.binding.reflectionTextureUniform.value = state.texture;
}
}
for (const hiddenWaterMesh of hiddenWaterMeshes) {
hiddenWaterMesh.mesh.visible = hiddenWaterMesh.visible;
}

View File

@@ -884,9 +884,20 @@ export class RuntimeHost {
const previousAutoClear = this.renderer.autoClear;
const previousRenderTarget = this.renderer.getRenderTarget();
const previousFogDensity = this.underwaterSceneFog.density;
const previousReflectionStates = this.runtimeWaterContactUniforms.map((waterBinding) => ({
binding: waterBinding,
enabled: waterBinding.reflectionEnabledUniform?.value ?? 0,
texture: waterBinding.reflectionTextureUniform?.value ?? null
}));
try {
this.underwaterSceneFog.density = 0;
for (const state of previousReflectionStates) {
if (state.binding.reflectionEnabledUniform !== null) {
state.binding.reflectionEnabledUniform.value = 0;
}
}
binding.reflectionTextureUniform.value = null;
this.renderer.autoClear = true;
this.renderer.setRenderTarget(binding.reflectionRenderTarget);
this.renderer.clear();
@@ -896,6 +907,14 @@ export class RuntimeHost {
this.renderer.autoClear = previousAutoClear;
this.modelGroup.visible = previousModelGroupVisibility;
this.underwaterSceneFog.density = previousFogDensity;
for (const state of previousReflectionStates) {
if (state.binding.reflectionEnabledUniform !== null) {
state.binding.reflectionEnabledUniform.value = state.enabled;
}
if (state.binding.reflectionTextureUniform !== null) {
state.binding.reflectionTextureUniform.value = state.texture;
}
}
for (const hiddenWaterMesh of hiddenWaterMeshes) {
hiddenWaterMesh.mesh.visible = hiddenWaterMesh.visible;

View File

@@ -3137,7 +3137,18 @@ export class ViewportHost {
const previousAutoClear = this.renderer.autoClear;
const previousRenderTarget = this.renderer.getRenderTarget();
const previousReflectionStates = this.viewportWaterSurfaceBindings.map((waterBinding) => ({
binding: waterBinding,
enabled: waterBinding.reflectionEnabledUniform?.value ?? 0,
texture: waterBinding.reflectionTextureUniform?.value ?? null
}));
try {
for (const state of previousReflectionStates) {
if (state.binding.reflectionEnabledUniform !== null) {
state.binding.reflectionEnabledUniform.value = 0;
}
}
binding.reflectionTextureUniform.value = null;
this.renderer.autoClear = true;
this.renderer.setRenderTarget(binding.reflectionRenderTarget);
this.renderer.clear();
@@ -3145,6 +3156,14 @@ export class ViewportHost {
} finally {
this.renderer.setRenderTarget(previousRenderTarget);
this.renderer.autoClear = previousAutoClear;
for (const state of previousReflectionStates) {
if (state.binding.reflectionEnabledUniform !== null) {
state.binding.reflectionEnabledUniform.value = state.enabled;
}
if (state.binding.reflectionTextureUniform !== null) {
state.binding.reflectionTextureUniform.value = state.texture;
}
}
for (const hiddenObject of hiddenObjects) {
hiddenObject.object.visible = hiddenObject.visible;