Add reflection support to water material

This commit is contained in:
2026-04-07 06:31:29 +02:00
parent d2bb70434b
commit 282c282f9c

View File

@@ -1,6 +1,20 @@
import { DoubleSide, Euler, MeshBasicMaterial, Quaternion, ShaderMaterial, Vector2, Vector3, Vector4 } from "three";
import {
DoubleSide,
Euler,
Matrix4,
MeshBasicMaterial,
Quaternion,
ShaderMaterial,
Texture,
UniformsLib,
UniformsUtils,
Vector2,
Vector3,
Vector4
} from "three";
import type { Vec3 } from "../core/vector";
import { MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT } from "../document/brushes";
export interface WaterContactBounds {
min: Vec3;
@@ -46,6 +60,15 @@ export interface WaterMaterialResult {
contactPatchesUniform: { value: Vector4[] } | null;
contactPatchAxesUniform: { value: Vector2[] } | null;
contactPatchShapesUniform: { value: number[] } | null;
reflectionTextureUniform: { value: Texture | null } | null;
reflectionMatrixUniform: { value: Matrix4 } | null;
reflectionEnabledUniform: { value: number } | null;
}
interface WaterMaterialReflectionOptions {
texture: Texture | null;
enabled: boolean;
strength?: number;
}
interface WaterMaterialOptions {
@@ -62,6 +85,7 @@ interface WaterMaterialOptions {
z: number;
};
contactPatches?: WaterContactPatch[];
reflection?: WaterMaterialReflectionOptions;
}
interface OrientedWaterVolume {
@@ -75,7 +99,7 @@ interface TriangleMeshSegmentSample {
normal: Vector3;
}
const MAX_WATER_CONTACT_PATCHES = 6;
const MAX_WATER_CONTACT_PATCHES = MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT;
const WATER_CONTACT_EPSILON = 1e-4;
function createBoundsCorners(bounds: WaterContactBounds) {
@@ -759,7 +783,7 @@ function appendTriangleMeshContactPatches(
patches.push(...mergeTriangleMeshContactPatches(rawPatches, bandMinimumThickness, source.mergeProfile));
}
export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[]): WaterContactPatch[] {
export function collectWaterContactPatches(volume: OrientedWaterVolume, contactBounds: WaterContactSource[], patchLimit = MAX_WATER_CONTACT_PATCHES): WaterContactPatch[] {
const inverseRotation = createInverseVolumeRotation(volume.rotationDegrees);
const halfX = Math.max(volume.size.x * 0.5, WATER_CONTACT_EPSILON);
const halfY = Math.max(volume.size.y * 0.5, WATER_CONTACT_EPSILON);
@@ -851,9 +875,11 @@ export function collectWaterContactPatches(volume: OrientedWaterVolume, contactB
}
}
const clampedPatchLimit = Math.max(1, Math.min(patchLimit, MAX_WATER_CONTACT_PATCHES));
return patches
.sort((left, right) => right.halfWidth * right.halfDepth - left.halfWidth * left.halfDepth)
.slice(0, MAX_WATER_CONTACT_PATCHES);
.slice(0, clampedPatchLimit);
}
export function createWaterContactPatchUniformValue(contactPatches?: WaterContactPatch[]): Vector4[] {
@@ -890,7 +916,10 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
animationUniform: null,
contactPatchesUniform: null,
contactPatchAxesUniform: null,
contactPatchShapesUniform: null
contactPatchShapesUniform: null,
reflectionTextureUniform: null,
reflectionMatrixUniform: null,
reflectionEnabledUniform: null
};
}
@@ -905,7 +934,10 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
animationUniform: null,
contactPatchesUniform: null,
contactPatchAxesUniform: null,
contactPatchShapesUniform: null
contactPatchShapesUniform: null,
reflectionTextureUniform: null,
reflectionMatrixUniform: null,
reflectionEnabledUniform: null
};
}
@@ -914,8 +946,12 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
const contactPatchesUniform = { value: createWaterContactPatchUniformValue(options.contactPatches) };
const contactPatchAxesUniform = { value: createWaterContactPatchAxisUniformValue(options.contactPatches) };
const contactPatchShapesUniform = { value: createWaterContactPatchShapeUniformValue(options.contactPatches) };
const reflectionTextureUniform = { value: options.reflection?.texture ?? null };
const reflectionMatrixUniform = { value: new Matrix4() };
const reflectionEnabledUniform = {
value: options.reflection?.enabled === true ? Math.max(0, Math.min(1, options.reflection?.strength ?? 0.36)) : 0
};
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));
const topFaceFlag = options.isTopFace ? 1 : 0;
const hex = options.colorHex.replace("#", "");
@@ -926,19 +962,21 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
const vertexShader = /* glsl */ `
uniform float time;
uniform float waveStrength;
uniform float waveAmplitude;
uniform float isTopFace;
uniform mat4 reflectionMatrix;
varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir;
varying vec4 vReflectionCoord;
#include <fog_pars_vertex>
void main() {
vec3 transformedPosition = position;
vLocalSurfaceUv = position.xz;
vWaveNormal = vec3(0.0, 1.0, 0.0);
vReflectionCoord = vec4(0.0);
if (isTopFace > 0.5) {
vec2 dirA = normalize(vec2(0.92, 0.38));
@@ -951,8 +989,6 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
float waveB = sin(phaseB) * 0.30;
float waveC = sin(phaseC) * 0.15;
transformedPosition.y += (waveA + waveB + waveC) * waveAmplitude;
vec2 slope =
dirA * (cos(phaseA) / 2.3) * 0.55 +
dirB * (cos(phaseB) / 1.45) * 0.30 +
@@ -964,6 +1000,7 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
vec4 mvPosition = viewMatrix * worldPos;
vWorldPos = worldPos.xyz;
vViewDir = normalize(cameraPosition - worldPos.xyz);
vReflectionCoord = reflectionMatrix * worldPos;
gl_Position = projectionMatrix * mvPosition;
#include <fog_vertex>
}
@@ -981,11 +1018,14 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
uniform vec4 contactPatches[${MAX_WATER_CONTACT_PATCHES}];
uniform vec2 contactPatchAxes[${MAX_WATER_CONTACT_PATCHES}];
uniform float contactPatchShapes[${MAX_WATER_CONTACT_PATCHES}];
uniform sampler2D reflectionTexture;
uniform float reflectionEnabled;
varying vec2 vLocalSurfaceUv;
varying vec3 vWaveNormal;
varying vec3 vWorldPos;
varying vec3 vViewDir;
varying vec4 vReflectionCoord;
#include <fog_pars_fragment>
float hash(vec2 p) {
@@ -1049,6 +1089,8 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
float contactFoam = 0.0;
float contactRipple = 0.0;
float contactSheen = 0.0;
float reflectionMask = 0.0;
vec3 reflectionColor = vec3(0.0);
float edgeDistance = min(halfSize.x - abs(vLocalSurfaceUv.x), halfSize.y - abs(vLocalSurfaceUv.y));
float edgeBand = max(0.22, min(halfSize.x, halfSize.y) * 0.12);
@@ -1099,9 +1141,21 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
float refraction = (largeWave - 0.5) * 0.18 + (mediumWave - 0.5) * 0.14 + (microWave - 0.5) * 0.08 + contactRipple * 0.06;
float glints = smoothstep(0.78, 0.97, fbm(vLocalSurfaceUv * 4.8 + normal.xz * 2.2 + vec2(time * 0.38, -time * 0.31))) * (0.14 + fresnel * 0.28);
vec3 color = mix(deepTint, shallowTint, clamp(0.46 + refraction + fresnel * 0.24 + caustics * 0.08, 0.05, 0.98));
if (isTopFace > 0.5 && reflectionEnabled > 0.0 && vReflectionCoord.w > 0.0) {
vec2 reflectionUv = vReflectionCoord.xy / vReflectionCoord.w;
reflectionUv += normal.xz * (0.01 + waveStrength * 0.012) + vec2((microWave - 0.5) * 0.018, (mediumWave - 0.5) * 0.015);
if (reflectionUv.x >= 0.0 && reflectionUv.x <= 1.0 && reflectionUv.y >= 0.0 && reflectionUv.y <= 1.0) {
reflectionColor = texture2D(reflectionTexture, clamp(reflectionUv, vec2(0.001), vec2(0.999))).rgb;
reflectionColor = mix(reflectionColor, shallowTint, 0.16);
reflectionMask = reflectionEnabled * clamp(0.12 + fresnel * 0.92 + glints * 0.28, 0.0, 0.92);
}
}
float foam = clamp(max(edgeFoam * 0.48, contactFoam) * (0.52 + waveStrength * 0.8) + caustics * 0.08 + glints * 0.06, 0.0, 0.84);
vec3 specular = vec3(pow(max(0.0, dot(reflect(-viewDir, normal), normalize(vec3(0.25, 0.88, 0.35)))), 18.0)) * (0.14 + fresnel * 0.56 + caustics * 0.14 + contactSheen * 0.12);
color = mix(color, reflectionColor, reflectionMask);
color = mix(color, vec3(0.97, 0.99, 1.0), foam);
color += specular;
color += vec3(0.05, 0.08, 0.12) * fresnel;
@@ -1116,21 +1170,28 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
}
`;
const material = new ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
const uniforms = UniformsUtils.merge([
UniformsLib.fog,
{
time: animationUniform,
waterColor: { value: [cr, cg, cb] },
surfaceOpacity: { value: clampedOpacity },
waveStrength: { value: waveStrength },
waveAmplitude: { value: waveAmplitude },
isTopFace: { value: topFaceFlag },
halfSize: { value: halfSize },
contactPatches: contactPatchesUniform,
contactPatchAxes: contactPatchAxesUniform,
contactPatchShapes: contactPatchShapesUniform
},
contactPatchShapes: contactPatchShapesUniform,
reflectionTexture: reflectionTextureUniform,
reflectionMatrix: reflectionMatrixUniform,
reflectionEnabled: reflectionEnabledUniform
}
]);
const material = new ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
transparent: true,
depthWrite: false,
fog: true,
@@ -1142,6 +1203,9 @@ export function createWaterMaterial(options: WaterMaterialOptions): WaterMateria
animationUniform,
contactPatchesUniform,
contactPatchAxesUniform,
contactPatchShapesUniform
contactPatchShapesUniform,
reflectionTextureUniform,
reflectionMatrixUniform,
reflectionEnabledUniform
};
}