UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

1,444 lines (1,262 loc) 66 kB
import { Script, Entity, Color, Texture, Vec3, ShaderMaterial, ShaderChunks, BLEND_NORMAL, CULLFACE_NONE, FOG_NONE, PIXELFORMAT_RGBA8, SEMANTIC_POSITION, SHADERLANGUAGE_GLSL, SHADERLANGUAGE_WGSL, LAYERID_DEPTH, LAYERID_UI } from 'playcanvas'; import { PlanarRenderer } from './planar-renderer.mjs'; // devices with the global water shader chunks already installed const _chunksInstalled = new WeakSet(); /** * Installs global shader chunk overrides used by the water on other materials in the scene: * * - fog is gated per pixel to fragments below the water level (define WATER_HEIGHT_FOG on the * receiving material), so an underwater fog color does not fog the world above the surface. * - animated caustics are applied to fragments below the water level (define WATER_CAUSTICS on * the receiving material), fading out with depth and following the directional light. * * The water script provides the global uniforms these use (waterLevel, waterTime, * waterCausticsMap, waterCausticsParams, waterCausticsColor). * * @param {import('playcanvas').GraphicsDevice} device - The graphics device. */ function setupWaterWorldChunks(device) { if (_chunksInstalled.has(device)) return; _chunksInstalled.add(device); // fog chunk override - matches the engine 'fogPS' chunk, with a per-pixel height gate const glslChunks = ShaderChunks.get(device, SHADERLANGUAGE_GLSL); glslChunks.set('fogPS', /* glsl */` float dBlendModeFogFactor = 1.0; #if (FOG != NONE) uniform vec3 fog_color; #if (FOG == LINEAR) uniform float fog_start; uniform float fog_end; #else uniform float fog_density; #endif #endif #ifdef WATER_HEIGHT_FOG uniform float waterFogLevel; #endif #ifdef VERTEXSHADER float getFogFactor(float depth) { #else float getFogFactor() { float depth = gl_FragCoord.z / gl_FragCoord.w; #endif float fogFactor = 0.0; #if (FOG == LINEAR) fogFactor = (fog_end - depth) / (fog_end - fog_start); #elif (FOG == EXP) fogFactor = exp(-depth * fog_density); #elif (FOG == EXP2) fogFactor = exp(-depth * depth * fog_density * fog_density); #endif return clamp(fogFactor, 0.0, 1.0); } #ifdef VERTEXSHADER vec3 addFog(vec3 color, float depth) { #if (FOG != NONE) return mix(fog_color * dBlendModeFogFactor, color, getFogFactor(depth)); #endif return color; } #else vec3 addFog(vec3 color) { #if (FOG != NONE) float fogFactor = getFogFactor(); #ifdef WATER_HEIGHT_FOG // no fog above the water level, soft blend across the waterline float below = clamp((waterFogLevel - vPositionW.y) * 4.0, 0.0, 1.0); fogFactor = mix(1.0, fogFactor, below); #endif return mix(fog_color * dBlendModeFogFactor, color, fogFactor); #endif return color; } #endif `); glslChunks.set('litUserDeclarationPS', /* glsl */` #if defined(WATER_CAUSTICS) && !defined(SHADOW_PASS) && !defined(PICK_PASS) && !defined(PREPASS_PASS) uniform float waterLevel; uniform sampler2D waterCausticsMap; uniform vec4 waterCausticsParams; // x: tiling, y: speed, z: strength, w: fade depth uniform vec3 waterCausticsColor; uniform float waterTime; #endif `); glslChunks.set('litUserMainEndPS', /* glsl */` #if defined(WATER_CAUSTICS) && !defined(SHADOW_PASS) && !defined(PICK_PASS) && !defined(PREPASS_PASS) { float depthBelow = waterLevel - vPositionW.y; if (depthBelow > 0.0) { vec2 cuv = vPositionW.xz * waterCausticsParams.x; float ct = waterTime * waterCausticsParams.y; float ca = texture2D(waterCausticsMap, cuv + vec2(ct, ct * 0.8)).x; float cb = texture2D(waterCausticsMap, cuv * 1.37 + vec2(-ct * 1.1, ct * 0.9)).y; float caustic = pow(clamp((ca + cb) * 0.5 * 2.2 - 0.9, 0.0, 1.0), 2.0); // fade in just below the waterline, fade out with depth, favor up-facing surfaces float gate = exp(-depthBelow / waterCausticsParams.w) * min(depthBelow * 4.0, 1.0); gate *= clamp(litArgs_worldNormal.y, 0.0, 1.0); gl_FragColor.rgb *= vec3(1.0) + waterCausticsColor * (caustic * waterCausticsParams.z * gate); } } #endif `); // WGSL equivalents const wgslChunks = ShaderChunks.get(device, SHADERLANGUAGE_WGSL); wgslChunks.set('fogPS', /* wgsl */` #include "fogMathPS" var<private> dBlendModeFogFactor : f32 = 1.0; #if (FOG != NONE) uniform fog_color : vec3f; #if (FOG == LINEAR) uniform fog_start : f32; uniform fog_end : f32; #else uniform fog_density : f32; #endif #endif #ifdef WATER_HEIGHT_FOG uniform waterFogLevel : f32; #endif #ifdef VERTEXSHADER fn getFogFactor(depth: f32) -> f32 { #else fn getFogFactor() -> f32 { let depth = pcPosition.z / pcPosition.w; #endif #if (FOG == LINEAR) return evaluateFogFactorLinear(depth, uniform.fog_start, uniform.fog_end); #elif (FOG == EXP) return evaluateFogFactorExp(depth, uniform.fog_density); #elif (FOG == EXP2) return evaluateFogFactorExp2(depth, uniform.fog_density); #else return 1.0; #endif } #ifdef VERTEXSHADER fn addFog(color: vec3f, depth: f32) -> vec3f { #if (FOG != NONE) return mix(uniform.fog_color * dBlendModeFogFactor, color, getFogFactor(depth)); #else return color; #endif } #else fn addFog(color: vec3f) -> vec3f { #if (FOG != NONE) var fogFactor = getFogFactor(); #ifdef WATER_HEIGHT_FOG // no fog above the water level, soft blend across the waterline let below = clamp((uniform.waterFogLevel - vPositionW.y) * 4.0, 0.0, 1.0); fogFactor = mix(1.0, fogFactor, below); #endif return mix(uniform.fog_color * dBlendModeFogFactor, color, fogFactor); #else return color; #endif } #endif `); wgslChunks.set('litUserDeclarationPS', /* wgsl */` #if defined(WATER_CAUSTICS) && !defined(SHADOW_PASS) && !defined(PICK_PASS) && !defined(PREPASS_PASS) uniform waterLevel : f32; var waterCausticsMap : texture_2d<f32>; var waterCausticsMapSampler : sampler; uniform waterCausticsParams : vec4f; // x: tiling, y: speed, z: strength, w: fade depth uniform waterCausticsColor : vec3f; uniform waterTime : f32; #endif `); wgslChunks.set('litUserMainEndPS', /* wgsl */` #if defined(WATER_CAUSTICS) && !defined(SHADOW_PASS) && !defined(PICK_PASS) && !defined(PREPASS_PASS) { // no branch here - textureSample requires uniform control flow in WGSL let depthBelow: f32 = uniform.waterLevel - vPositionW.y; let cuv: vec2f = vPositionW.xz * uniform.waterCausticsParams.x; let ct: f32 = uniform.waterTime * uniform.waterCausticsParams.y; let ca: f32 = textureSample(waterCausticsMap, waterCausticsMapSampler, cuv + vec2f(ct, ct * 0.8)).x; let cb: f32 = textureSample(waterCausticsMap, waterCausticsMapSampler, cuv * 1.37 + vec2f(-ct * 1.1, ct * 0.9)).y; let caustic: f32 = pow(clamp((ca + cb) * 0.5 * 2.2 - 0.9, 0.0, 1.0), 2.0); // fade in just below the waterline, fade out with depth, favor up-facing surfaces var gate: f32 = exp(-max(depthBelow, 0.0) / uniform.waterCausticsParams.w) * clamp(depthBelow * 4.0, 0.0, 1.0); gate *= clamp(litArgs_worldNormal.y, 0.0, 1.0); output.color = vec4f(output.color.rgb * (vec3f(1.0) + uniform.waterCausticsColor * (caustic * uniform.waterCausticsParams.z * gate)), output.color.a); } #endif `); } // Reusable objects to avoid allocations const _flippedNormal = new Vec3(); // ---------------------- // Shared wave functions - Gerstner waves, 3 octaves derived from the main direction // ---------------------- const waveFunctionsGLSL = /* glsl */` #ifdef WATER_WAVES uniform vec4 uWaveParams; // x: amplitude, y: frequency, z: speed, w: steepness uniform vec2 uWaveDir; uniform vec4 uSwellParams; // x: amplitude, y: frequency, z: speed, w: steepness uniform vec2 uSwellDir; uniform float uTime; // per wave octave: x: direction angle offset (rad), y: amplitude mult, z: frequency mult, // w: speed mult. Irrational-ish frequency ratios and irregular directions and phases avoid // a visible repeating interference pattern. const int CHOP_COUNT = 3; const vec4 CHOP_OCTAVES[CHOP_COUNT] = vec4[CHOP_COUNT]( vec4( 0.00, 1.00, 1.00, 1.00), vec4( 0.41, 0.55, 1.73, 0.87), vec4(-0.87, 0.32, 2.61, 0.79) ); const int SWELL_COUNT = 2; const vec4 SWELL_OCTAVES[SWELL_COUNT] = vec4[SWELL_COUNT]( vec4( 0.00, 1.00, 1.00, 1.00), vec4(-0.51, 0.65, 0.63, 1.13) ); vec2 waveRotate(vec2 v, float a) { float c = cos(a); float s = sin(a); return vec2(c * v.x - s * v.y, s * v.x + c * v.y); } // slowly varying spatial amplitude envelope, different per octave - stops the wave field // from looking uniform and repeating over large distances float waveEnvelope(vec2 p, vec2 dir, float freq, float seed) { vec2 envDir = waveRotate(dir, 1.07); return 0.55 + 0.45 * sin(freq * 0.17 * dot(envDir, p) + seed * 4.7); } vec3 gerstnerDisplace(vec2 p, vec4 octave, vec4 params, vec2 mainDir, float seed) { vec2 dir = waveRotate(mainDir, octave.x); float freq = params.y * octave.z; float amp = params.x * octave.y * waveEnvelope(p, dir, freq, seed); float th = freq * dot(dir, p) + params.z * octave.w * freq * uTime + seed * 2.3; return vec3(dir.x * params.w * amp * cos(th), amp * sin(th), dir.y * params.w * amp * cos(th)); } vec3 gerstnerNormal(vec2 p, vec4 octave, vec4 params, vec2 mainDir, float seed) { vec2 dir = waveRotate(mainDir, octave.x); float freq = params.y * octave.z; float amp = params.x * octave.y * waveEnvelope(p, dir, freq, seed); float th = freq * dot(dir, p) + params.z * octave.w * freq * uTime + seed * 2.3; return vec3(-dir.x * freq * amp * cos(th), -params.w * freq * amp * sin(th), -dir.y * freq * amp * cos(th)); } vec3 waveDisplacement(vec2 p) { vec3 d = vec3(0.0); for (int i = 0; i < CHOP_COUNT; i++) { d += gerstnerDisplace(p, CHOP_OCTAVES[i], uWaveParams, uWaveDir, float(i)); } for (int i = 0; i < SWELL_COUNT; i++) { d += gerstnerDisplace(p, SWELL_OCTAVES[i], uSwellParams, uSwellDir, float(i) + 5.0); } return d; } vec3 waveNormal(vec2 p) { vec3 n = vec3(0.0, 1.0, 0.0); for (int i = 0; i < CHOP_COUNT; i++) { n += gerstnerNormal(p, CHOP_OCTAVES[i], uWaveParams, uWaveDir, float(i)); } for (int i = 0; i < SWELL_COUNT; i++) { n += gerstnerNormal(p, SWELL_OCTAVES[i], uSwellParams, uSwellDir, float(i) + 5.0); } return n; } #endif `; const waveFunctionsWGSL = /* wgsl */` #ifdef WATER_WAVES uniform uWaveParams: vec4f; // x: amplitude, y: frequency, z: speed, w: steepness uniform uWaveDir: vec2f; uniform uSwellParams: vec4f; // x: amplitude, y: frequency, z: speed, w: steepness uniform uSwellDir: vec2f; uniform uTime: f32; // per wave octave: x: direction angle offset (rad), y: amplitude mult, z: frequency mult, // w: speed mult. Irrational-ish frequency ratios and irregular directions and phases avoid // a visible repeating interference pattern. const CHOP_COUNT: i32 = 3; const CHOP_OCTAVES = array<vec4f, 3>( vec4f( 0.00, 1.00, 1.00, 1.00), vec4f( 0.41, 0.55, 1.73, 0.87), vec4f(-0.87, 0.32, 2.61, 0.79) ); const SWELL_COUNT: i32 = 2; const SWELL_OCTAVES = array<vec4f, 2>( vec4f( 0.00, 1.00, 1.00, 1.00), vec4f(-0.51, 0.65, 0.63, 1.13) ); fn waveRotate(v: vec2f, a: f32) -> vec2f { let c: f32 = cos(a); let s: f32 = sin(a); return vec2f(c * v.x - s * v.y, s * v.x + c * v.y); } // slowly varying spatial amplitude envelope, different per octave - stops the wave field // from looking uniform and repeating over large distances fn waveEnvelope(p: vec2f, dir: vec2f, freq: f32, seed: f32) -> f32 { let envDir: vec2f = waveRotate(dir, 1.07); return 0.55 + 0.45 * sin(freq * 0.17 * dot(envDir, p) + seed * 4.7); } fn gerstnerDisplace(p: vec2f, octave: vec4f, params: vec4f, mainDir: vec2f, seed: f32) -> vec3f { let dir: vec2f = waveRotate(mainDir, octave.x); let freq: f32 = params.y * octave.z; let amp: f32 = params.x * octave.y * waveEnvelope(p, dir, freq, seed); let th: f32 = freq * dot(dir, p) + params.z * octave.w * freq * uniform.uTime + seed * 2.3; return vec3f(dir.x * params.w * amp * cos(th), amp * sin(th), dir.y * params.w * amp * cos(th)); } fn gerstnerNormal(p: vec2f, octave: vec4f, params: vec4f, mainDir: vec2f, seed: f32) -> vec3f { let dir: vec2f = waveRotate(mainDir, octave.x); let freq: f32 = params.y * octave.z; let amp: f32 = params.x * octave.y * waveEnvelope(p, dir, freq, seed); let th: f32 = freq * dot(dir, p) + params.z * octave.w * freq * uniform.uTime + seed * 2.3; return vec3f(-dir.x * freq * amp * cos(th), -params.w * freq * amp * sin(th), -dir.y * freq * amp * cos(th)); } fn waveDisplacement(p: vec2f) -> vec3f { var d: vec3f = vec3f(0.0); for (var i: i32 = 0; i < CHOP_COUNT; i++) { d += gerstnerDisplace(p, CHOP_OCTAVES[i], uniform.uWaveParams, uniform.uWaveDir, f32(i)); } for (var i: i32 = 0; i < SWELL_COUNT; i++) { d += gerstnerDisplace(p, SWELL_OCTAVES[i], uniform.uSwellParams, uniform.uSwellDir, f32(i) + 5.0); } return d; } fn waveNormal(p: vec2f) -> vec3f { var n: vec3f = vec3f(0.0, 1.0, 0.0); for (var i: i32 = 0; i < CHOP_COUNT; i++) { n += gerstnerNormal(p, CHOP_OCTAVES[i], uniform.uWaveParams, uniform.uWaveDir, f32(i)); } for (var i: i32 = 0; i < SWELL_COUNT; i++) { n += gerstnerNormal(p, SWELL_OCTAVES[i], uniform.uSwellParams, uniform.uSwellDir, f32(i) + 5.0); } return n; } #endif `; // ---------------------- // GLSL Shaders // ---------------------- const vertexGLSL = /* glsl */` #ifdef WATER_DEPTH_EFFECTS #include "screenDepthPS" #endif attribute vec3 vertex_position; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; varying vec3 vWorldPos; varying vec4 vScreenPos; #ifdef WATER_WAVES varying vec2 vWavePos; #endif #ifdef WATER_DEPTH_EFFECTS varying float vViewDepth; #endif ${waveFunctionsGLSL} void main(void) { vec4 worldPos = matrix_model * vec4(vertex_position, 1.0); #ifdef WATER_WAVES vWavePos = worldPos.xz; worldPos.xyz += waveDisplacement(worldPos.xz); #endif vWorldPos = worldPos.xyz; vec4 projPos = matrix_viewProjection * worldPos; gl_Position = projPos; vScreenPos = projPos; #ifdef WATER_DEPTH_EFFECTS vViewDepth = getLinearDepth(worldPos.xyz); #endif } `; const fragmentGLSL = /* glsl */` #include "gammaPS" #ifdef WATER_DEPTH_EFFECTS #include "screenDepthPS" #endif #ifdef WATER_REFLECTION_SKY #include "decodePS" #include "envAtlasPS" #include "sphericalPS" #endif #ifndef SCREENSIZE #define SCREENSIZE uniform vec4 uScreenSize; #endif uniform sampler2D uNormalMap; #ifdef WATER_REFLECTION_PLANAR uniform sampler2D uReflectionMap; #endif #ifdef WATER_REFLECTION_SKY uniform sampler2D texture_envAtlas; uniform float uSkyBlur; #endif #ifdef WATER_REFRACTION uniform sampler2D uRefractionMap; #endif uniform vec3 view_position; #ifndef WATER_WAVES uniform float uTime; #endif uniform vec3 uShallowColor; uniform vec3 uDeepColor; uniform vec3 uLightDir; uniform vec3 uLightColor; uniform float uRippleTiling; uniform float uRippleSpeed; uniform float uBumpiness; uniform float uDistortion; uniform float uFresnelPower; uniform float uReflectionStrength; uniform float uDepthFade; uniform float uShoreSoftness; uniform float uOpacity; uniform float uSpecularPower; uniform float uSpecularIntensity; uniform float uDiffuseIntensity; uniform float uUnderwaterFogDensity; uniform vec3 uUnderwaterFogColor; uniform float uCameraBelow; uniform float uSnellWindow; #ifdef WATER_FOAM uniform vec3 uFoamColor; uniform float uFoamDepth; #endif varying vec3 vWorldPos; varying vec4 vScreenPos; #ifdef WATER_WAVES varying vec2 vWavePos; #endif #ifdef WATER_DEPTH_EFFECTS varying float vViewDepth; #endif ${waveFunctionsGLSL} void main(void) { // two layers of scrolling detail normals, in tangent space of a horizontal plane. The // second layer is rotated, so the tiling of the two never lines up into a visible grid. vec2 baseUv = vWorldPos.xz * uRippleTiling; float st = uTime * uRippleSpeed; vec2 rotUv = vec2(0.79 * baseUv.x - 0.61 * baseUv.y, 0.61 * baseUv.x + 0.79 * baseUv.y); vec3 nm1 = texture2D(uNormalMap, baseUv + vec2(st, st * 0.7)).xyz * 2.0 - 1.0; vec3 nm2 = texture2D(uNormalMap, rotUv * 1.73 + vec2(-st * 0.8, st * 1.1)).xyz * 2.0 - 1.0; vec2 detail = (nm1.xy + nm2.xy) * 0.5 * uBumpiness; // combine with the geometry wave normal #ifdef WATER_WAVES vec3 waveN = waveNormal(vWavePos); #else vec3 waveN = vec3(0.0, 1.0, 0.0); #endif vec3 N = normalize(vec3(waveN.x + detail.x, waveN.y, waveN.z + detail.y)); vec3 V = normalize(view_position - vWorldPos); float NdotV = max(dot(N, V), 0.0); // screen uv to sample the planar textures. The refraction camera matches the main camera, // so its texture is sampled without a flip; the reflection camera is mirrored by the // water plane, which flips the image vertically. vec2 screenUV = gl_FragCoord.xy * uScreenSize.zw; vec2 refractionUV = screenUV + N.xz * uDistortion; vec2 reflectionUV = vec2(screenUV.x, 1.0 - screenUV.y) + N.xz * uDistortion; // sample the planar / sky reflection textures up front, before any divergent flow // (WGSL uniformity). Both the distorted and undistorted refraction samples are taken - // the undistorted one is the fallback when the distortion reaches past a silhouette // into above-water content #ifdef WATER_REFRACTION vec3 refrSample = texture2D(uRefractionMap, refractionUV).rgb; vec3 refrUndistorted = texture2D(uRefractionMap, screenUV).rgb; #else vec3 refrSample = uShallowColor; #endif vec3 reflSample = uDeepColor; #ifdef WATER_REFLECTION_PLANAR reflSample = texture2D(uReflectionMap, reflectionUV).rgb; #endif #ifdef WATER_REFLECTION_SKY vec3 R = reflect(-V, N); R.y = abs(R.y); // no scene below the horizon, mirror the sky instead vec2 skyUv = toSphericalUv(R * vec3(-1.0, 1.0, 1.0)); float level = clamp(uSkyBlur, 0.0, 4.0); float ilevel = floor(level); vec3 skyA = decodeRGBP(texture2D(texture_envAtlas, mapRoughnessUv(skyUv, ilevel))); vec3 skyB = decodeRGBP(texture2D(texture_envAtlas, mapRoughnessUv(skyUv, ilevel + 1.0))); reflSample = mix(skyA, skyB, level - ilevel); #endif // surface seen from below: blend the above-water world (refraction texture, visible in // the Snell window overhead) with total internal reflection of the underwater scene // (reflection texture), and fade to the underwater fog color with distance. Selected by // the camera side rather than per pixel, as at grazing angles the perturbed normal can // flip against the view direction, causing speckles if (uCameraBelow > 0.5) { float NdotUp = max(dot(-N, V), 0.0); float windowCenter = 1.0 - uSnellWindow; float window = smoothstep(windowCenter - 0.07, windowCenter + 0.07, NdotUp); vec3 col = mix(reflSample, refrSample, window); float dist = length(view_position - vWorldPos); col = mix(uUnderwaterFogColor, col, exp(-dist * uUnderwaterFogDensity)); gl_FragColor = vec4(gammaCorrectOutput(col), 1.0); return; } // water depth (distance the view ray travels under water) based effects float waterDepth = 0.0; float absorb = 0.8; #ifdef WATER_DEPTH_EFFECTS vec2 depthUv = getGrabScreenPos(vScreenPos); float sceneDepth = getLinearScreenDepth(depthUv + N.xz * uDistortion); if (sceneDepth < vViewDepth) { // distorted sample is above the water surface, fall back to undistorted - both // for the depth and for the refraction color, so above-water content (or the sky // behind clipped geometry) does not leak into the shore line sceneDepth = getLinearScreenDepth(depthUv); #ifdef WATER_REFRACTION refrSample = refrUndistorted; #endif } waterDepth = max(sceneDepth - vViewDepth, 0.0); absorb = 1.0 - exp(-waterDepth / uDepthFade); #endif // sun diffuse - modulates the water body color vec3 L = -uLightDir; vec3 sunDiffuse = mix(vec3(1.0), uLightColor * max(dot(N, L), 0.0), uDiffuseIntensity); vec3 shallowColor = uShallowColor * sunDiffuse; vec3 deepColor = uDeepColor * sunDiffuse; // base color of the water body - refracted scene absorbed with depth #ifdef WATER_REFRACTION vec3 base = mix(refrSample * shallowColor, deepColor, absorb); #else vec3 base = mix(shallowColor, deepColor, absorb); #endif // reflection vec3 refl = vec3(0.0); float hasReflection = 0.0; #if defined(WATER_REFLECTION_PLANAR) || defined(WATER_REFLECTION_SKY) refl = reflSample; hasReflection = 1.0; #endif // fresnel blend between the water body and the reflection float fresnel = mix(0.02, 1.0, pow(1.0 - NdotV, uFresnelPower)); vec3 color = mix(base, refl, hasReflection * clamp(fresnel * uReflectionStrength, 0.0, 1.0)); // sun specular vec3 H = normalize(L + V); color += uLightColor * pow(max(dot(N, H), 0.0), uSpecularPower) * uSpecularIntensity; // foam along the shoreline, lit by the directional light so it dims at night. The extra // light-luminance factor keeps the white foam from glowing against the dark night scene #if defined(WATER_FOAM) && defined(WATER_DEPTH_EFFECTS) float foamNoise = clamp(0.5 + 0.5 * (nm1.x + nm2.y), 0.0, 1.0); float foamMask = 1.0 - smoothstep(0.0, uFoamDepth, waterDepth); foamMask = smoothstep(0.3, 0.7, foamMask * (0.4 + 0.6 * foamNoise)); // intensity follows the light luminance, but the foam keeps its own color (no orange // foam at sunrise) float lightLum = dot(uLightColor, vec3(0.299, 0.587, 0.114)); float foamAtten = clamp(lightLum, 0.0, 1.0); vec3 foamLit = uFoamColor * (max(dot(N, L), 0.0) * lightLum * foamAtten * foamAtten * foamAtten); color = mix(color, foamLit, foamMask); #endif // opacity - fully opaque when refraction supplies the background #ifdef WATER_REFRACTION float alpha = 1.0; #else float alpha = mix(uOpacity, 1.0, clamp(fresnel, 0.0, 1.0)); #endif // soft intersection with the geometry along the shoreline #ifdef WATER_DEPTH_EFFECTS alpha *= clamp(waterDepth / uShoreSoftness, 0.0, 1.0); #endif gl_FragColor = vec4(gammaCorrectOutput(color), alpha); } `; // ---------------------- // WGSL Shaders // ---------------------- const vertexWGSL = /* wgsl */` #ifdef WATER_DEPTH_EFFECTS #include "screenDepthPS" #endif attribute vertex_position: vec3f; uniform matrix_model: mat4x4f; uniform matrix_viewProjection: mat4x4f; varying vWorldPos: vec3f; varying vScreenPos: vec4f; #ifdef WATER_WAVES varying vWavePos: vec2f; #endif #ifdef WATER_DEPTH_EFFECTS varying vViewDepth: f32; #endif ${waveFunctionsWGSL} @vertex fn vertexMain(input: VertexInput) -> VertexOutput { var output: VertexOutput; var worldPos: vec4f = uniform.matrix_model * vec4f(input.vertex_position, 1.0); #ifdef WATER_WAVES output.vWavePos = worldPos.xz; worldPos = vec4f(worldPos.xyz + waveDisplacement(worldPos.xz), 1.0); #endif output.vWorldPos = worldPos.xyz; let projPos: vec4f = uniform.matrix_viewProjection * worldPos; output.position = projPos; output.vScreenPos = projPos; #ifdef WATER_DEPTH_EFFECTS output.vViewDepth = getLinearDepth(worldPos.xyz); #endif return output; } `; const fragmentWGSL = /* wgsl */` #include "gammaPS" #ifdef WATER_DEPTH_EFFECTS #include "screenDepthPS" #endif #ifdef WATER_REFLECTION_SKY #include "decodePS" #include "envAtlasPS" #include "sphericalPS" #endif #ifndef SCREENSIZE #define SCREENSIZE uniform uScreenSize: vec4f; #endif var uNormalMap: texture_2d<f32>; var uNormalMapSampler: sampler; #ifdef WATER_REFLECTION_PLANAR var uReflectionMap: texture_2d<f32>; var uReflectionMapSampler: sampler; #endif #ifdef WATER_REFLECTION_SKY var texture_envAtlas: texture_2d<f32>; var texture_envAtlasSampler: sampler; uniform uSkyBlur: f32; #endif #ifdef WATER_REFRACTION var uRefractionMap: texture_2d<f32>; var uRefractionMapSampler: sampler; #endif uniform view_position: vec3f; #ifndef WATER_WAVES uniform uTime: f32; #endif uniform uShallowColor: vec3f; uniform uDeepColor: vec3f; uniform uLightDir: vec3f; uniform uLightColor: vec3f; uniform uRippleTiling: f32; uniform uRippleSpeed: f32; uniform uBumpiness: f32; uniform uDistortion: f32; uniform uFresnelPower: f32; uniform uReflectionStrength: f32; uniform uDepthFade: f32; uniform uShoreSoftness: f32; uniform uOpacity: f32; uniform uSpecularPower: f32; uniform uSpecularIntensity: f32; uniform uDiffuseIntensity: f32; uniform uUnderwaterFogDensity: f32; uniform uUnderwaterFogColor: vec3f; uniform uCameraBelow: f32; uniform uSnellWindow: f32; #ifdef WATER_FOAM uniform uFoamColor: vec3f; uniform uFoamDepth: f32; #endif varying vWorldPos: vec3f; varying vScreenPos: vec4f; #ifdef WATER_WAVES varying vWavePos: vec2f; #endif #ifdef WATER_DEPTH_EFFECTS varying vViewDepth: f32; #endif ${waveFunctionsWGSL} @fragment fn fragmentMain(input: FragmentInput) -> FragmentOutput { var output: FragmentOutput; // two layers of scrolling detail normals, in tangent space of a horizontal plane. The // second layer is rotated, so the tiling of the two never lines up into a visible grid. let baseUv: vec2f = input.vWorldPos.xz * uniform.uRippleTiling; let st: f32 = uniform.uTime * uniform.uRippleSpeed; let rotUv: vec2f = vec2f(0.79 * baseUv.x - 0.61 * baseUv.y, 0.61 * baseUv.x + 0.79 * baseUv.y); let nm1: vec3f = textureSample(uNormalMap, uNormalMapSampler, baseUv + vec2f(st, st * 0.7)).xyz * 2.0 - 1.0; let nm2: vec3f = textureSample(uNormalMap, uNormalMapSampler, rotUv * 1.73 + vec2f(-st * 0.8, st * 1.1)).xyz * 2.0 - 1.0; let detail: vec2f = (nm1.xy + nm2.xy) * 0.5 * uniform.uBumpiness; // combine with the geometry wave normal #ifdef WATER_WAVES let waveN: vec3f = waveNormal(input.vWavePos); #else let waveN: vec3f = vec3f(0.0, 1.0, 0.0); #endif let N: vec3f = normalize(vec3f(waveN.x + detail.x, waveN.y, waveN.z + detail.y)); let V: vec3f = normalize(uniform.view_position - input.vWorldPos); let NdotV: f32 = max(dot(N, V), 0.0); // screen uv to sample the planar textures. The refraction camera matches the main camera, // so its texture is sampled without a flip; the reflection camera is mirrored by the // water plane, which flips the image vertically. let screenUV: vec2f = pcPosition.xy * uniform.uScreenSize.zw; let refractionUV: vec2f = screenUV + N.xz * uniform.uDistortion; let reflectionUV: vec2f = vec2f(screenUV.x, 1.0 - screenUV.y) + N.xz * uniform.uDistortion; // sample the planar / sky reflection textures up front, before any divergent flow // (WGSL uniformity). Both the distorted and undistorted refraction samples are taken - // the undistorted one is the fallback when the distortion reaches past a silhouette // into above-water content #ifdef WATER_REFRACTION var refrSample: vec3f = textureSample(uRefractionMap, uRefractionMapSampler, refractionUV).rgb; let refrUndistorted: vec3f = textureSample(uRefractionMap, uRefractionMapSampler, screenUV).rgb; #else let refrSample: vec3f = uniform.uShallowColor; #endif var reflSample: vec3f = uniform.uDeepColor; #ifdef WATER_REFLECTION_PLANAR reflSample = textureSample(uReflectionMap, uReflectionMapSampler, reflectionUV).rgb; #endif #ifdef WATER_REFLECTION_SKY var R: vec3f = reflect(-V, N); R.y = abs(R.y); // no scene below the horizon, mirror the sky instead let skyUv: vec2f = toSphericalUv(R * vec3f(-1.0, 1.0, 1.0)); let level: f32 = clamp(uniform.uSkyBlur, 0.0, 4.0); let ilevel: f32 = floor(level); let skyA: vec3f = decodeRGBP(textureSample(texture_envAtlas, texture_envAtlasSampler, mapRoughnessUv(skyUv, ilevel))); let skyB: vec3f = decodeRGBP(textureSample(texture_envAtlas, texture_envAtlasSampler, mapRoughnessUv(skyUv, ilevel + 1.0))); reflSample = mix(skyA, skyB, level - ilevel); #endif // surface seen from below: blend the above-water world (refraction texture, visible in // the Snell window overhead) with total internal reflection of the underwater scene // (reflection texture), and fade to the underwater fog color with distance. Selected by // the camera side rather than per pixel, as at grazing angles the perturbed normal can // flip against the view direction, causing speckles if (uniform.uCameraBelow > 0.5) { let NdotUp: f32 = max(dot(-N, V), 0.0); let windowCenter: f32 = 1.0 - uniform.uSnellWindow; let window: f32 = smoothstep(windowCenter - 0.07, windowCenter + 0.07, NdotUp); var col: vec3f = mix(reflSample, refrSample, window); let dist: f32 = length(uniform.view_position - input.vWorldPos); col = mix(uniform.uUnderwaterFogColor, col, exp(-dist * uniform.uUnderwaterFogDensity)); output.color = vec4f(gammaCorrectOutput(col), 1.0); return output; } // water depth (distance the view ray travels under water) based effects var waterDepth: f32 = 0.0; var absorb: f32 = 0.8; #ifdef WATER_DEPTH_EFFECTS let depthUv: vec2f = getGrabScreenPos(input.vScreenPos); var sceneDepth: f32 = getLinearScreenDepth(depthUv + N.xz * uniform.uDistortion); if (sceneDepth < input.vViewDepth) { // distorted sample is above the water surface, fall back to undistorted - both // for the depth and for the refraction color, so above-water content (or the sky // behind clipped geometry) does not leak into the shore line sceneDepth = getLinearScreenDepth(depthUv); #ifdef WATER_REFRACTION refrSample = refrUndistorted; #endif } waterDepth = max(sceneDepth - input.vViewDepth, 0.0); absorb = 1.0 - exp(-waterDepth / uniform.uDepthFade); #endif // sun diffuse - modulates the water body color let L: vec3f = -uniform.uLightDir; let sunDiffuse: vec3f = mix(vec3f(1.0), uniform.uLightColor * max(dot(N, L), 0.0), uniform.uDiffuseIntensity); let shallowColor: vec3f = uniform.uShallowColor * sunDiffuse; let deepColor: vec3f = uniform.uDeepColor * sunDiffuse; // base color of the water body - refracted scene absorbed with depth #ifdef WATER_REFRACTION let base: vec3f = mix(refrSample * shallowColor, deepColor, absorb); #else let base: vec3f = mix(shallowColor, deepColor, absorb); #endif // reflection var refl: vec3f = vec3f(0.0); var hasReflection: f32 = 0.0; #if defined(WATER_REFLECTION_PLANAR) || defined(WATER_REFLECTION_SKY) refl = reflSample; hasReflection = 1.0; #endif // fresnel blend between the water body and the reflection let fresnel: f32 = mix(0.02, 1.0, pow(1.0 - NdotV, uniform.uFresnelPower)); var color: vec3f = mix(base, refl, hasReflection * clamp(fresnel * uniform.uReflectionStrength, 0.0, 1.0)); // sun specular let H: vec3f = normalize(L + V); color += uniform.uLightColor * pow(max(dot(N, H), 0.0), uniform.uSpecularPower) * uniform.uSpecularIntensity; // foam along the shoreline, lit by the directional light so it dims at night. The extra // light-luminance factor keeps the white foam from glowing against the dark night scene #if defined(WATER_FOAM) && defined(WATER_DEPTH_EFFECTS) let foamNoise: f32 = clamp(0.5 + 0.5 * (nm1.x + nm2.y), 0.0, 1.0); var foamMask: f32 = 1.0 - smoothstep(0.0, uniform.uFoamDepth, waterDepth); foamMask = smoothstep(0.3, 0.7, foamMask * (0.4 + 0.6 * foamNoise)); // intensity follows the light luminance, but the foam keeps its own color (no orange // foam at sunrise) let lightLum: f32 = dot(uniform.uLightColor, vec3f(0.299, 0.587, 0.114)); let foamAtten: f32 = clamp(lightLum, 0.0, 1.0); let foamLit: vec3f = uniform.uFoamColor * (max(dot(N, L), 0.0) * lightLum * foamAtten * foamAtten * foamAtten); color = mix(color, foamLit, foamMask); #endif // opacity - fully opaque when refraction supplies the background #ifdef WATER_REFRACTION var alpha: f32 = 1.0; #else var alpha: f32 = mix(uniform.uOpacity, 1.0, clamp(fresnel, 0.0, 1.0)); #endif // soft intersection with the geometry along the shoreline #ifdef WATER_DEPTH_EFFECTS alpha *= clamp(waterDepth / uniform.uShoreSoftness, 0.0, 1.0); #endif output.color = vec4f(gammaCorrectOutput(color), alpha); return output; } `; // Note: material.setParameter stores arrays by reference, so each uniform needs its own array /** * Water script renders a configurable water surface on the entity it is attached to. The entity * needs a render component with a plane type mesh (or a custom, roughly horizontal, plane-like * mesh - when geometry waves are used, the mesh needs to be tessellated). It supports: * * - Planar reflection of the scene, sky-dome only reflection, or no reflection. * - Planar refraction of the scene, or a simple transparency-based alternative. * - Optional oblique-clipping of the reflected / refracted scene by the water plane. * - Depth-based effects: color absorption, soft shorelines and foam. These require the scene * depth map, and are enabled on the main camera automatically. * - Optional geometry waves (Gerstner), displacing the mesh vertices and lighting. * - Diffuse and specular lighting from a single directional light. * * The water entity should be placed on its own layer which is rendered after the skybox - the * internal reflection and refraction cameras render all layers of the main camera excluding * the water's own layers, the UI and the depth layer. * * @example * const water = new Entity('Water'); * water.addComponent('render', { type: 'plane', layers: [waterLayer.id], castShadows: false }); * water.setLocalScale(100, 1, 100); * water.addComponent('script'); * water.script.create(Water, { * properties: { * cameraEntity: camera, * lightEntity: sun, * normalMap: assets.normalMap.resource * } * }); * app.root.addChild(water); * @category Rendering */ class Water extends Script { static scriptName = 'water'; /** * The entity containing the main camera of the scene. * * @attribute * @type {Entity} */ cameraEntity = null; /** * The entity with a directional light component, used for the diffuse and specular lighting * of the water. When not set, the water is not lit by the sun. * * @attribute * @type {Entity} */ lightEntity = null; /** * The normal map used for the animated ripple detail of the surface. Two scrolling layers of * it are combined. When not set, a flat normal map is used. * * @type {Texture|null} */ normalMap = null; /** * The greyscale pattern used for the underwater caustics. Two scrolling layers of it are * combined. When not set, the normal map is used as the pattern instead. * * @type {Texture|null} */ causticsMap = null; /** * The source of the reflection: 'planar' renders the scene mirrored by the water plane, * 'sky' reflects the sky-dome (scene envAtlas) only, 'none' disables reflection. * * @attribute * @type {string} */ reflectionSource = 'planar'; /** * If set to true, the scene is rendered into a refraction texture, seen through the water, * distorted by the surface ripples. When disabled, simple alpha transparency is used instead, * which is cheaper. * * @attribute * @type {boolean} */ refraction = true; /** * If set to true, the reflection / refraction cameras use an oblique projection to clip the * scene by the water plane, so no geometry from the wrong side of the surface leaks into the * textures. * * @attribute * @type {boolean} */ obliqueClipping = true; /** * Distance the oblique clipping plane is shifted, allowing geometry slightly past the water * plane to be rendered. This hides artifacts along the waterline, especially with waves. * * @attribute * @range [0, 2] * @precision 2 * @step 0.01 */ clipBias = 0.1; /** * Resolution of the reflection / refraction textures, relative to the main camera render * buffer resolution. * * @attribute * @range [0.05, 1] * @precision 2 * @step 0.05 */ textureScale = 0.5; /** * If set to true, depth based effects are enabled: color absorption with depth, soft * shoreline intersections and foam. Requires the scene depth map, which is automatically * requested on the main camera. * * @attribute * @type {boolean} */ depthEffects = true; /** * Color of the shallow water, tinting the refracted scene. * * @attribute */ shallowColor = new Color(0.6, 0.9, 0.85); /** * Color of the deep water, which the water fades to with depth. * * @attribute */ deepColor = new Color(0.03, 0.16, 0.24); /** * Distance in world units over which the water absorbs the light and fades to the deep color. * * @attribute * @range [0.1, 50] * @precision 1 * @step 0.1 */ depthFade = 3; /** * Distance in world units over which the water fades out where it intersects geometry. * * @attribute * @range [0.01, 5] * @precision 2 * @step 0.01 */ shoreSoftness = 0.3; /** * Opacity of the water, used only when refraction is disabled. * * @attribute * @range [0, 1] * @precision 2 * @step 0.05 */ opacity = 0.8; /** * Tiling of the ripple normal map, in repeats per world unit. * * @attribute * @range [0.001, 2] * @precision 3 * @step 0.01 */ rippleTiling = 0.1; /** * Scrolling speed of the ripple normal map layers. * * @attribute * @range [0, 1] * @precision 3 * @step 0.005 */ rippleSpeed = 0.05; /** * Strength of the ripple normal map perturbation. * * @attribute * @range [0, 2] * @precision 2 * @step 0.05 */ bumpiness = 0.5; /** * Amount the surface normal distorts the reflection and refraction. * * @attribute * @range [0, 0.2] * @precision 3 * @step 0.005 */ distortion = 0.03; /** * Power of the fresnel term - how quickly the reflection fades out as the view angle * approaches the surface normal. * * @attribute * @range [0.5, 16] * @precision 1 * @step 0.5 */ fresnelPower = 5; /** * Strength of the reflection. * * @attribute * @range [0, 2] * @precision 2 * @step 0.05 */ reflectionStrength = 1; /** * Blurriness of the sky reflection (0 = sharp), used only when reflectionSource is 'sky'. * * @attribute * @range [0, 4] * @precision 1 * @step 0.1 */ skyBlur = 0; /** * Power of the specular highlight of the directional light - higher values create a smaller, * sharper sun glint. * * @attribute * @range [8, 2048] * @precision 0 * @step 8 */ specularPower = 256; /** * Intensity of the specular highlight of the directional light. * * @attribute * @range [0, 4] * @precision 2 * @step 0.05 */ specularIntensity = 1; /** * How much the directional light diffusely lights the water body. * * @attribute * @range [0, 1] * @precision 2 * @step 0.05 */ diffuseIntensity = 0.35; /** * If set to true, foam is rendered along the shoreline. Requires depthEffects. * * @attribute * @type {boolean} */ foam = true; /** * Color of the shoreline foam. * * @attribute */ foamColor = new Color(1, 1, 1); /** * Water depth in world units below which the shoreline foam shows. * * @attribute * @range [0.01, 5] * @precision 2 * @step 0.01 */ foamDepth = 0.5; /** * Tiling of the caustics pattern, in repeats per world unit. The caustics render on other * materials in the scene which have the WATER_CAUSTICS define set, using the global shader * chunks and uniforms this script provides. * * @attribute * @range [0.01, 1] * @precision 3 * @step 0.01 */ causticsTiling = 0.09; /** * Scrolling speed of the caustics pattern. * * @attribute * @range [0, 0.5] * @precision 3 * @step 0.005 */ causticsSpeed = 0.04; /** * Brightness of the caustics. * * @attribute * @range [0, 4] * @precision 2 * @step 0.05 */ causticsStrength = 1.5; /** * Water depth in world units over which the caustics fade out. * * @attribute * @range [0.1, 20] * @precision 1 * @step 0.1 */ causticsDepth = 3; /** * Size of the Snell window - how much of the world above the surface is visible when looking * up from under the water, with the rest showing the reflection of the underwater scene. * The physically correct value is around 0.35. * * @attribute * @range [0.05, 0.95] * @precision 2 * @step 0.05 */ snellWindow = 0.35; /** * If set to true, the water mesh is displaced by Gerstner waves, and the wave shape affects * the lighting. Requires a tessellated plane mesh for the displacement to be visible. * * @attribute * @type {boolean} */ waves = false; /** * Amplitude of the main wave in world units. * * @attribute * @range [0, 5] * @precision 2 * @step 0.01 */ waveAmplitude = 0.15; /** * Length of the main wave in world units. * * @attribute * @range [0.5, 100] * @precision 1 * @step 0.5 */ waveLength = 8; /** * Speed multiplier of the wave animation. * * @attribute * @range [0, 5] * @precision 2 * @step 0.05 */ waveSpeed = 1; /** * Steepness of the waves - how much the vertices are also displaced horizontally, creating * sharper crests. * * @attribute * @range [0, 1] * @precision 2 * @step 0.05 */ waveSteepness = 0.4; /** * Direction the main wave travels, as an angle in degrees around the up axis. * * @attribute * @range [0, 360] * @precision 0 * @step 1 */ waveDirection = 0; /** * Amplitude of the swell - a second band of longer, slower waves layered on top of the main * waves. In world units. * * @attribute * @range [0, 5] * @precision 2 * @step 0.01 */ swellAmplitude = 0.25; /** * Length of the swell waves in world units. * * @attribute * @range [1, 200] * @precision 1 * @step 0.5 */ swellLength = 35; /** * Speed multiplier of the swell animation. * * @attribute * @range [0, 5] * @precision 2 * @step 0.05 */ swellSpeed = 1; /** * Direction the swell travels, as an angle in degrees around the up axis. * * @attribute * @range [0, 360] * @precision 0 * @step 1 */ swellDirection = 40; /** @private */ _material = null; /** @private */ _originalMaterials = []; /** @private */ _reflectionEntity = null; /** @private */ _reflectionRenderer = null; /** @private */ _refractionEntity = null; /** @private */ _refractionRenderer = null; /** @private */ _fallbackNormalMap = null; /** @private */ _depthMapRequested = false; /** @private */ _time = 0; // uniform value arrays, one per uniform as they are stored by reference /** @private */ _shallowColorArray = [0, 0, 0]; /** @private */ _deepColorArray = [0, 0, 0]; /** @private */ _lightDirArray = [0, -1, 0]; /** @private */ _lightColorArray = [0, 0, 0]; /** @private */ _foamColorArray = [0, 0, 0]; /** @private */ _waveParamsArray = [0, 0, 0, 0]; /** @private */ _waveDirArray = [1, 0]; /** @private */ _swellParamsArray = [0, 0, 0, 0]; /** @private */ _swellDirArray = [1, 0]; /** @private */ _causticsParamsArray = [0, 0, 0, 0]; /** @private */ _underwaterFogColorArray = [0, 0, 0]; in