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_negX_posY: ["negX_posY_negZ", "negX_posY_posZ"],
edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"] edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"]
}; };
const WATER_TOP_FACE_RENDER_SEGMENTS = 12;
function cloneVec3(vector) { function cloneVec3(vector) {
return { x: vector.x, y: vector.y, z: vector.z }; return { x: vector.x, y: vector.y, z: vector.z };
} }
@@ -222,6 +223,29 @@ function computeFaceBounds(vertices) {
} }
return { min, max }; 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) { export function getBoxBrushFaceVertexIds(faceId) {
return FACE_VERTEX_IDS[faceId]; return FACE_VERTEX_IDS[faceId];
} }
@@ -257,6 +281,7 @@ export function buildBoxBrushDerivedMeshData(brush) {
const normal = computeNewellNormal(faceVertices); const normal = computeNewellNormal(faceVertices);
const faceBounds = computeFaceBounds(faceVertices); const faceBounds = computeFaceBounds(faceVertices);
const uvSize = getFaceUvSize(faceId, faceBounds); const uvSize = getFaceUvSize(faceId, faceBounds);
const uvState = brush.faces[faceId].uv;
const indexStart = indices.length; const indexStart = indices.length;
faceSurfaces.push({ faceSurfaces.push({
faceId, faceId,
@@ -264,15 +289,32 @@ export function buildBoxBrushDerivedMeshData(brush) {
triangles, triangles,
normal normal
}); });
for (const triangle of triangles) { const useSubdividedWaterTopFace = brush.volume.mode === "water" && faceId === "posY" && brush.volume.water.surfaceDisplacementEnabled;
for (const vertexOffset of triangle) { if (useSubdividedWaterTopFace) {
const vertex = faceVertices[vertexOffset]; const faceCorners = faceVertices;
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds); for (let row = 0; row < WATER_TOP_FACE_RENDER_SEGMENTS; row += 1) {
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, brush.faces[faceId].uv); const v0 = row / WATER_TOP_FACE_RENDER_SEGMENTS;
positions.push(vertex.x, vertex.y, vertex.z); const v1 = (row + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
normals.push(normal.x, normal.y, normal.z); for (let column = 0; column < WATER_TOP_FACE_RENDER_SEGMENTS; column += 1) {
uvs.push(transformedUv.x, transformedUv.y); const u0 = column / WATER_TOP_FACE_RENDER_SEGMENTS;
indices.push(indices.length); 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({ 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"] edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"]
}; };
const WATER_TOP_FACE_RENDER_SEGMENTS = 12;
export interface BoxBrushGeometryDiagnostic { export interface BoxBrushGeometryDiagnostic {
code: string; code: string;
message: string; message: string;
@@ -280,6 +282,50 @@ function computeFaceBounds(vertices: Vec3[]): { min: Vec3; max: Vec3 } {
return { min, max }; 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] { export function getBoxBrushFaceVertexIds(faceId: BoxFaceId): readonly [BoxVertexId, BoxVertexId, BoxVertexId, BoxVertexId] {
return FACE_VERTEX_IDS[faceId]; return FACE_VERTEX_IDS[faceId];
} }
@@ -322,6 +368,7 @@ export function buildBoxBrushDerivedMeshData(brush: BoxBrush): DerivedBoxBrushMe
const normal = computeNewellNormal(faceVertices); const normal = computeNewellNormal(faceVertices);
const faceBounds = computeFaceBounds(faceVertices); const faceBounds = computeFaceBounds(faceVertices);
const uvSize = getFaceUvSize(faceId, faceBounds); const uvSize = getFaceUvSize(faceId, faceBounds);
const uvState = brush.faces[faceId].uv as FaceUvState;
const indexStart = indices.length; const indexStart = indices.length;
faceSurfaces.push({ faceSurfaces.push({
@@ -331,15 +378,49 @@ export function buildBoxBrushDerivedMeshData(brush: BoxBrush): DerivedBoxBrushMe
normal normal
}); });
for (const triangle of triangles) { const useSubdividedWaterTopFace =
for (const vertexOffset of triangle) { brush.volume.mode === "water" &&
const vertex = faceVertices[vertexOffset]; faceId === "posY" &&
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds); brush.volume.water.surfaceDisplacementEnabled;
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, brush.faces[faceId].uv as FaceUvState);
positions.push(vertex.x, vertex.y, vertex.z); if (useSubdividedWaterTopFace) {
normals.push(normal.x, normal.y, normal.z); const faceCorners = faceVertices as [Vec3, Vec3, Vec3, Vec3];
uvs.push(transformedUv.x, transformedUv.y);
indices.push(indices.length); 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 vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal; varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir; varying vec3 vViewDir;
varying vec4 vReflectionCoord; varying vec4 vReflectionCoord;
#include <fog_pars_vertex> #include <fog_pars_vertex>
@@ -754,7 +753,6 @@ export function createWaterMaterial(options) {
vec4 worldPos = modelMatrix * vec4(transformedPosition, 1.0); vec4 worldPos = modelMatrix * vec4(transformedPosition, 1.0);
vec4 mvPosition = viewMatrix * worldPos; vec4 mvPosition = viewMatrix * worldPos;
vWorldPos = worldPos.xyz;
vViewDir = normalize(cameraPosition - worldPos.xyz); vViewDir = normalize(cameraPosition - worldPos.xyz);
vReflectionCoord = reflectionMatrix * worldPos; vReflectionCoord = reflectionMatrix * worldPos;
gl_Position = projectionMatrix * mvPosition; gl_Position = projectionMatrix * mvPosition;
@@ -779,7 +777,6 @@ export function createWaterMaterial(options) {
varying vec2 vLocalSurfaceUv; varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal; varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir; varying vec3 vViewDir;
varying vec4 vReflectionCoord; varying vec4 vReflectionCoord;
#include <fog_pars_fragment> #include <fog_pars_fragment>

View File

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

View File

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

View File

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

View File

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