@driftengine/core 4.8.7 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +61 -0
- package/README.md +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/render/atmosphere.d.ts +13 -0
- package/dist/render/atmosphere.js +9 -1
- package/dist/render/backend/api.d.ts +9 -1
- package/dist/render/backend/createRenderer.d.ts +6 -0
- package/dist/render/backend/createRenderer.js +4 -1
- package/dist/render/backend/webgl2/renderer.d.ts +90 -2
- package/dist/render/backend/webgl2/renderer.js +297 -16
- package/dist/render/backend/webgpu/device.d.ts +7 -1
- package/dist/render/backend/webgpu/device.js +31 -3
- package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
- package/dist/render/backend/webgpu/flatPass.js +58 -26
- package/dist/render/backend/webgpu/postPass.d.ts +51 -9
- package/dist/render/backend/webgpu/postPass.js +16 -2
- package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
- package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
- package/dist/render/backend/webgpu/renderer.d.ts +131 -2
- package/dist/render/backend/webgpu/renderer.js +586 -70
- package/dist/render/displayRange.d.ts +31 -0
- package/dist/render/displayRange.js +47 -0
- package/dist/render/filmicCurve.d.ts +74 -0
- package/dist/render/filmicCurve.js +203 -0
- package/dist/render/fog.d.ts +4 -1
- package/dist/render/fog.js +12 -2
- package/dist/render/frame/resources.d.ts +2 -2
- package/dist/render/frame/resources.js +6 -0
- package/dist/render/frameReflections.d.ts +40 -0
- package/dist/render/frameReflections.js +48 -0
- package/dist/render/pass.d.ts +49 -0
- package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
- package/dist/render/reflectionSurfaceTargets.js +99 -0
- package/dist/render/renderQuality.d.ts +39 -8
- package/dist/render/renderQuality.js +4 -0
- package/dist/render/sceneTarget.d.ts +5 -0
- package/dist/render/sceneTarget.js +24 -1
- package/dist/render/screenSpaceReflection.d.ts +5 -0
- package/dist/render/screenSpaceReflection.js +5 -0
- package/dist/render/shaders/arcane.d.ts +1 -1
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/caustics.d.ts +1 -1
- package/dist/render/shaders/film.d.ts +1 -1
- package/dist/render/shaders/filmLook.js +2 -1
- package/dist/render/shaders/fire.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +2 -0
- package/dist/render/shaders/flat/index.d.ts +10 -0
- package/dist/render/shaders/flat/index.js +6 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +28 -0
- package/dist/render/shaders/flat/overlay.d.ts +16 -0
- package/dist/render/shaders/flat/overlay.js +117 -0
- package/dist/render/shaders/flat/pointShadow.js +1 -1
- package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
- package/dist/render/shaders/flat/reflectionSurface.js +61 -0
- package/dist/render/shaders/fog.d.ts +1 -1
- package/dist/render/shaders/fog.js +9 -2
- package/dist/render/shaders/fringe.d.ts +29 -0
- package/dist/render/shaders/fringe.js +55 -0
- package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
- package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/film.wgsl.js +1 -1
- package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/fire.wgsl.js +1 -1
- package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -16
- package/dist/render/shaders/generated/flat.wgsl.js +214 -54
- package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/line.wgsl.js +1 -1
- package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/particle.wgsl.js +4 -4
- package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
- package/dist/render/shaders/generated/rush.wgsl.js +56 -26
- package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
- package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
- package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
- package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
- package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/water.wgsl.js +1 -1
- package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
- package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
- package/dist/render/shaders/line.d.ts +1 -1
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/rush.js +54 -11
- package/dist/render/shaders/scatter.d.ts +1 -1
- package/dist/render/shaders/smoke.d.ts +1 -1
- package/dist/render/shaders/ssrResolve.d.ts +17 -0
- package/dist/render/shaders/ssrResolve.js +51 -0
- package/dist/render/shaders/ssrTrace.js +26 -0
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/ssrPass.d.ts +16 -0
- package/dist/render/ssrPass.js +106 -1
- package/dist/render/surfaceOverlay.d.ts +132 -0
- package/dist/render/surfaceOverlay.js +85 -0
- package/dist/render/surfaceTexture.d.ts +19 -0
- package/dist/render/surfaceTexture.js +69 -0
- package/dist/render/temporalAa.d.ts +14 -0
- package/dist/render/temporalAa.js +20 -0
- package/dist/render/vertexDefaults.d.ts +17 -0
- package/dist/render/vertexDefaults.js +24 -0
- package/package.json +6 -6
- package/src/index.ts +16 -0
- package/src/render/atmosphere.ts +22 -1
- package/src/render/backend/api.ts +9 -0
- package/src/render/backend/createRenderer.ts +18 -1
- package/src/render/backend/webgl2/renderer.ts +357 -13
- package/src/render/backend/webgpu/device.ts +42 -3
- package/src/render/backend/webgpu/flatPass.ts +63 -26
- package/src/render/backend/webgpu/postPass.ts +18 -0
- package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
- package/src/render/backend/webgpu/renderer.ts +735 -83
- package/src/render/displayRange.ts +61 -0
- package/src/render/filmicCurve.ts +229 -0
- package/src/render/fog.ts +19 -3
- package/src/render/frame/resources.ts +6 -0
- package/src/render/frameReflections.ts +72 -0
- package/src/render/pass.ts +53 -1
- package/src/render/reflectionSurfaceTargets.ts +105 -0
- package/src/render/renderQuality.ts +44 -8
- package/src/render/sceneTarget.ts +30 -2
- package/src/render/screenSpaceReflection.ts +5 -0
- package/src/render/shaders/filmLook.ts +2 -1
- package/src/render/shaders/flat/glassTint.ts +5 -1
- package/src/render/shaders/flat/index.ts +16 -0
- package/src/render/shaders/flat/main.ts +28 -0
- package/src/render/shaders/flat/overlay.ts +117 -0
- package/src/render/shaders/flat/pointShadow.ts +1 -1
- package/src/render/shaders/flat/reflectionSurface.ts +61 -0
- package/src/render/shaders/fog.ts +9 -2
- package/src/render/shaders/fringe.ts +59 -0
- package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
- package/src/render/shaders/generated/film.wgsl.ts +1 -1
- package/src/render/shaders/generated/fire.wgsl.ts +1 -1
- package/src/render/shaders/generated/flat.wgsl.ts +214 -54
- package/src/render/shaders/generated/line.wgsl.ts +1 -1
- package/src/render/shaders/generated/particle.wgsl.ts +4 -4
- package/src/render/shaders/generated/rush.wgsl.ts +56 -26
- package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
- package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
- package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
- package/src/render/shaders/generated/water.wgsl.ts +1 -1
- package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
- package/src/render/shaders/rush.ts +54 -11
- package/src/render/shaders/ssrResolve.ts +52 -0
- package/src/render/shaders/ssrTrace.ts +26 -0
- package/src/render/ssrPass.ts +138 -1
- package/src/render/surfaceOverlay.ts +195 -0
- package/src/render/surfaceTexture.ts +67 -0
- package/src/render/temporalAa.ts +20 -0
- package/src/render/vertexDefaults.ts +28 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Construction-time GPU quality controls shared by every renderer resource. */
|
|
2
2
|
|
|
3
|
+
import type { FrameReflections } from './frameReflections.ts';
|
|
3
4
|
import { MAX_AREA_LIGHTS } from './areaLights.ts';
|
|
4
5
|
import { MAX_GLOBAL_MEDIUM_STEPS } from './globalMedium.ts';
|
|
5
6
|
import { MAX_POINT_LIGHTS } from './lightBudget.ts';
|
|
@@ -26,8 +27,14 @@ import { PREFILTER_SAMPLE_COUNTS } from './prefilterEnvMap.ts';
|
|
|
26
27
|
* red where `srgb` clips each channel and turns its core pink and wide. It is the curve a ported
|
|
27
28
|
* game's own resolve applied — for a port that has to match one — and it has none of ACES's toe or
|
|
28
29
|
* desaturation. See `HIGHLIGHT_SHOULDER_GLSL` for the arithmetic.
|
|
30
|
+
*
|
|
31
|
+
* `filmic` is a film curve shaped by five numbers — a slope, a toe, a shoulder and how far black
|
|
32
|
+
* and white clip — that holds a mid grey of 0.18 at 0.18 where `aces` gives about 0.11, so a look
|
|
33
|
+
* graded against such a curve is matched by its numbers (`setFilmicCurve`) rather than by a lookup
|
|
34
|
+
* table. Applied by the composite alone, so it needs `screenEffects` and `hdrScene`; without them
|
|
35
|
+
* every pass grades with `aces` and the renderer says so once. See `filmicCurve.ts`.
|
|
29
36
|
*/
|
|
30
|
-
export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder';
|
|
37
|
+
export type OutputTransform = 'none' | 'srgb' | 'aces' | 'shoulder' | 'filmic';
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
40
|
* The three settings whose accepted values are a closed set, written as one.
|
|
@@ -355,6 +362,18 @@ export interface RenderQuality {
|
|
|
355
362
|
* eight-bit and this buys only the consistent grading.
|
|
356
363
|
*/
|
|
357
364
|
readonly hdrScene: boolean;
|
|
365
|
+
/**
|
|
366
|
+
* Ask for a high dynamic range display: a canvas whose highlights run past paper white to as
|
|
367
|
+
* far as the display allows, where it reports one. Off by default.
|
|
368
|
+
*
|
|
369
|
+
* **Asked for, not assumed.** It needs WebGPU, a display reporting a high dynamic range, and a
|
|
370
|
+
* composite that holds the light to give it (`screenEffects` and `hdrScene`); where any of those
|
|
371
|
+
* is missing the frame goes out as it always did and the renderer says which in
|
|
372
|
+
* `displayRangeReason`. `displayRange` is the answer a game reads to choose a look graded for the
|
|
373
|
+
* output it got. `setDisplayLuminance` sets how far above paper white the highlights may reach.
|
|
374
|
+
* WebGL2 has no extended-range canvas to draw into, so there it is always the standard range.
|
|
375
|
+
*/
|
|
376
|
+
readonly highDynamicRange: boolean;
|
|
358
377
|
/** Camera-depth water tint and scattering across every visible pass. */
|
|
359
378
|
readonly underwaterAtmosphere: boolean;
|
|
360
379
|
/** Procedural noise octaves evaluated by plume shaders. */
|
|
@@ -522,14 +541,14 @@ export interface RenderQuality {
|
|
|
522
541
|
*/
|
|
523
542
|
readonly sceneSamples: number;
|
|
524
543
|
/**
|
|
525
|
-
*
|
|
544
|
+
* Motion blur, 0 to 1. Off by default.
|
|
526
545
|
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
* needs only the previous view-projection,
|
|
532
|
-
*
|
|
546
|
+
* By the camera's motion everywhere, and **by each drawn object's own motion where a
|
|
547
|
+
* reconstruction draws one** (DriftTR, WebGPU): a mover's pixels smear along its path while a
|
|
548
|
+
* still stage stays sharp. Elsewhere a velocity buffer would be every mesh writing its motion into
|
|
549
|
+
* a second target — a structural change to a forward renderer — so those frames blur by the
|
|
550
|
+
* camera alone, which needs only the previous view-projection, and say so once when a draw names
|
|
551
|
+
* where it was. `setCameraMotionBlur` is the per-frame dial and the longest smear.
|
|
533
552
|
*
|
|
534
553
|
* Runs inside the composite pass, so it costs a depth sample and eight taps on the pixels
|
|
535
554
|
* that are moving and nothing on a still frame. Needs `screenEffects`, since without the
|
|
@@ -580,6 +599,18 @@ export interface RenderQuality {
|
|
|
580
599
|
* Needs `screenEffects`, since the resolve composites over the finished scene.
|
|
581
600
|
*/
|
|
582
601
|
readonly orderIndependent: boolean;
|
|
602
|
+
/**
|
|
603
|
+
* Every opaque lit surface reflects the frame by its own material: a ray marched against the
|
|
604
|
+
* frame's depth swaps the probes' share of each pixel for what it finds, at the pixel's own
|
|
605
|
+
* roughness and reflectance, and a surface past `maxRoughness` keeps the probes alone. Off by
|
|
606
|
+
* default; `true` for the defaults, or the numbers. See `frameReflections.ts`.
|
|
607
|
+
*
|
|
608
|
+
* Needs `screenEffects` and `hdrScene`, since the swap is made in scene light, and a frame of one
|
|
609
|
+
* sample; it says once where it cannot be drawn. What it costs on WebGPU is every opaque lit draw
|
|
610
|
+
* a second time without its lamps, into two half-float targets, and a march per reflective pixel;
|
|
611
|
+
* on WebGL2 the same two targets, written beside the colour by the opaque lit draws themselves.
|
|
612
|
+
*/
|
|
613
|
+
readonly screenSpaceReflections: boolean | FrameReflections;
|
|
583
614
|
/**
|
|
584
615
|
* Depth of field: how wide the blur may reach, as a fraction of the frame's height. Off by
|
|
585
616
|
* default.
|
|
@@ -964,9 +995,11 @@ export const DEFAULT_RENDER_QUALITY: Readonly<RenderQuality> = Object.freeze({
|
|
|
964
995
|
sceneSamples: 1,
|
|
965
996
|
planarReflections: false,
|
|
966
997
|
hdrScene: false,
|
|
998
|
+
highDynamicRange: false,
|
|
967
999
|
cameraMotionBlur: 0,
|
|
968
1000
|
temporalAa: false,
|
|
969
1001
|
orderIndependent: false,
|
|
1002
|
+
screenSpaceReflections: false,
|
|
970
1003
|
depthOfField: 0,
|
|
971
1004
|
reconstruction: 0,
|
|
972
1005
|
indirectLight: false,
|
|
@@ -1126,12 +1159,15 @@ export function resolveRenderQuality(options: RenderQualityOptions = {}): Readon
|
|
|
1126
1159
|
),
|
|
1127
1160
|
planarReflections: options.planarReflections ?? DEFAULT_RENDER_QUALITY.planarReflections,
|
|
1128
1161
|
hdrScene: options.hdrScene ?? DEFAULT_RENDER_QUALITY.hdrScene,
|
|
1162
|
+
highDynamicRange: options.highDynamicRange ?? DEFAULT_RENDER_QUALITY.highDynamicRange,
|
|
1129
1163
|
cameraMotionBlur: Math.min(
|
|
1130
1164
|
1,
|
|
1131
1165
|
Math.max(0, options.cameraMotionBlur ?? DEFAULT_RENDER_QUALITY.cameraMotionBlur),
|
|
1132
1166
|
),
|
|
1133
1167
|
temporalAa: options.temporalAa ?? DEFAULT_RENDER_QUALITY.temporalAa,
|
|
1134
1168
|
orderIndependent: options.orderIndependent ?? DEFAULT_RENDER_QUALITY.orderIndependent,
|
|
1169
|
+
screenSpaceReflections:
|
|
1170
|
+
options.screenSpaceReflections ?? DEFAULT_RENDER_QUALITY.screenSpaceReflections,
|
|
1135
1171
|
/* Off, or a ratio at least 1.3 and at most 2 — never a number between zero and the range. */
|
|
1136
1172
|
reconstruction: resolveReconstruction(options.reconstruction),
|
|
1137
1173
|
indirectLight: options.indirectLight ?? DEFAULT_RENDER_QUALITY.indirectLight,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { glSceneDepthFormat } from './depthConvention.ts';
|
|
2
2
|
import { clampGrain, clampVignette, grainSeed } from './shaders/filmLook.ts';
|
|
3
|
+
import { FRINGE_FLOATS, resolveFringe } from './shaders/fringe.ts';
|
|
3
4
|
import { AmbientOcclusionPass } from './ambientOcclusionPass.ts';
|
|
4
5
|
import { ExposurePass } from './exposurePass.ts';
|
|
5
6
|
import { clampAutoExposure } from './shaders/exposure.ts';
|
|
@@ -99,6 +100,8 @@ export class SceneTarget {
|
|
|
99
100
|
private gradeStrength = 0;
|
|
100
101
|
/** The lens and the print, held like the grade. See `filmLook.ts`. */
|
|
101
102
|
private vignette = 0;
|
|
103
|
+
/** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
|
|
104
|
+
private readonly fringe = new Float32Array(FRINGE_FLOATS);
|
|
102
105
|
private grain = 0;
|
|
103
106
|
private grainSeed = 0;
|
|
104
107
|
/** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
|
|
@@ -702,6 +705,11 @@ export class SceneTarget {
|
|
|
702
705
|
this.vignette = clampVignette(strength);
|
|
703
706
|
}
|
|
704
707
|
|
|
708
|
+
/** The lens's colour fringe, held until changed. See `Renderer.setChromaticAberration`. */
|
|
709
|
+
setChromaticAberration(intensity: number, start: number): void {
|
|
710
|
+
resolveFringe(intensity, start, this.fringe);
|
|
711
|
+
}
|
|
712
|
+
|
|
705
713
|
/** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
|
|
706
714
|
setFilmGrain(strength: number, seed: number): void {
|
|
707
715
|
this.grain = clampGrain(strength);
|
|
@@ -769,8 +777,15 @@ export class SceneTarget {
|
|
|
769
777
|
resolve(
|
|
770
778
|
strength: number,
|
|
771
779
|
reachUv: number,
|
|
772
|
-
/*
|
|
773
|
-
|
|
780
|
+
/*
|
|
781
|
+
* The grade, applied here because this is the last pass. See RUSH_FRAG's own note. `film` is
|
|
782
|
+
* the `filmic` curve's eight constants (`resolveFilmicCurve`), read only under transform 4.
|
|
783
|
+
*/
|
|
784
|
+
grade: {
|
|
785
|
+
readonly transform: number;
|
|
786
|
+
readonly exposure: number;
|
|
787
|
+
readonly film?: Float32Array;
|
|
788
|
+
} = {
|
|
774
789
|
transform: 0,
|
|
775
790
|
exposure: 1,
|
|
776
791
|
},
|
|
@@ -1012,6 +1027,12 @@ export class SceneTarget {
|
|
|
1012
1027
|
gl.uniform1f(this.uniforms['uReach'] ?? null, reachUv);
|
|
1013
1028
|
gl.uniform1i(this.uniforms['uOutputTransform'] ?? null, grade.transform);
|
|
1014
1029
|
gl.uniform1f(this.uniforms['uOutputExposure'] ?? null, grade.exposure);
|
|
1030
|
+
if (grade.film !== undefined) {
|
|
1031
|
+
gl.uniform4fv(this.uniforms['uFilmA'] ?? null, grade.film, 0, 4);
|
|
1032
|
+
gl.uniform4fv(this.uniforms['uFilmB'] ?? null, grade.film, 4, 4);
|
|
1033
|
+
}
|
|
1034
|
+
/* A standard range always here: see `Renderer.displayRange`. */
|
|
1035
|
+
gl.uniform1f(this.uniforms['uDisplayHeadroom'] ?? null, 1);
|
|
1015
1036
|
|
|
1016
1037
|
/*
|
|
1017
1038
|
* Depth on its own unit, and the reprojection with it. Bound every frame rather than
|
|
@@ -1113,6 +1134,12 @@ export class SceneTarget {
|
|
|
1113
1134
|
this.uniforms['uLocalExposure'] ?? null,
|
|
1114
1135
|
localGrid === null ? 0 : this.localExposure,
|
|
1115
1136
|
);
|
|
1137
|
+
/* Each object's own motion is WebGPU's under a reconstruction; here the scene stands in on
|
|
1138
|
+
unit 7 and the flag says not to read it, so the camera's motion alone blurs. */
|
|
1139
|
+
gl.activeTexture(gl.TEXTURE7);
|
|
1140
|
+
gl.bindTexture(gl.TEXTURE_2D, scene);
|
|
1141
|
+
gl.uniform1i(this.uniforms['uMotion'] ?? null, 7);
|
|
1142
|
+
gl.uniform1f(this.uniforms['uObjectMotion'] ?? null, 0);
|
|
1116
1143
|
gl.activeTexture(gl.TEXTURE0);
|
|
1117
1144
|
|
|
1118
1145
|
/* No unit and no texture: a flat colour, not a sample. Uploaded unconditionally — two
|
|
@@ -1122,6 +1149,7 @@ export class SceneTarget {
|
|
|
1122
1149
|
gl.uniform3fv(this.uniforms['uVeilColor'] ?? null, veilColor);
|
|
1123
1150
|
gl.uniform1f(this.uniforms['uVeilAlpha'] ?? null, veilAlpha);
|
|
1124
1151
|
gl.uniform1f(this.uniforms['uVignette'] ?? null, this.vignette);
|
|
1152
|
+
gl.uniform3fv(this.uniforms['uFringe'] ?? null, this.fringe);
|
|
1125
1153
|
gl.uniform2f(this.uniforms['uGrain'] ?? null, this.grain, this.grainSeed);
|
|
1126
1154
|
|
|
1127
1155
|
gl.bindVertexArray(this.vao);
|
|
@@ -27,6 +27,11 @@ import type { DecalBox } from './decalProjector.ts';
|
|
|
27
27
|
* which is the same price the temporal row declined for a velocity buffer. A caller declares a
|
|
28
28
|
* `ReflectiveSurface` instead: a box, and a facing rule for which surfaces inside it reflect. It is
|
|
29
29
|
* the shape `DecalProjector` already has, for the same reason.
|
|
30
|
+
*
|
|
31
|
+
* **Or every surface by its own material**, where the renderer was built with
|
|
32
|
+
* `screenSpaceReflections`: the lit stage leaves two maps of what it reflected — not a G-buffer, the
|
|
33
|
+
* environment's share and its tint, which is all a swap needs — and the same trace runs in a
|
|
34
|
+
* material mode over every pixel. See `frameReflections.ts`.
|
|
30
35
|
*/
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -88,7 +88,8 @@ export function grainWeight(luma: number): number {
|
|
|
88
88
|
|
|
89
89
|
/** Both backends' words when there is no composite to apply the look in. */
|
|
90
90
|
export const FILM_LOOK_WITHOUT_COMPOSITE =
|
|
91
|
-
'[driftengine] setVignette and
|
|
91
|
+
'[driftengine] setVignette, setFilmGrain and setChromaticAberration need `screenEffects`: ' +
|
|
92
|
+
'without a composite every ' +
|
|
92
93
|
'pass is the last thing to touch the frame, so there is no one place to apply a lens or a ' +
|
|
93
94
|
'print. The look was not applied.';
|
|
94
95
|
|
|
@@ -34,7 +34,9 @@ export type LitSwitch =
|
|
|
34
34
|
| 'LIGHT_FIXTURES'
|
|
35
35
|
| 'SURFACE_EFFECTS'
|
|
36
36
|
| 'DRIFT_LIGHT'
|
|
37
|
-
| 'PHYSICAL_SPECULAR'
|
|
37
|
+
| 'PHYSICAL_SPECULAR'
|
|
38
|
+
| 'SURFACE_OVERLAY'
|
|
39
|
+
| 'REFLECTION_MAPS';
|
|
38
40
|
|
|
39
41
|
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
40
42
|
export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
@@ -45,6 +47,8 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
|
45
47
|
SURFACE_EFFECTS: false,
|
|
46
48
|
DRIFT_LIGHT: false,
|
|
47
49
|
PHYSICAL_SPECULAR: false,
|
|
50
|
+
SURFACE_OVERLAY: false,
|
|
51
|
+
REFLECTION_MAPS: false,
|
|
48
52
|
};
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -19,6 +19,8 @@ import { AREA_LIGHT_GLSL } from './areaLight.ts';
|
|
|
19
19
|
import { NORMAL_MAP_GLSL } from './normalMap.ts';
|
|
20
20
|
import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
|
|
21
21
|
import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.ts';
|
|
22
|
+
import { overlayGlsl } from './overlay.ts';
|
|
23
|
+
import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
|
|
22
24
|
import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.ts';
|
|
23
25
|
import { FULL_LIGHT_BUDGET, type LightBudget } from '../../uniformVectorBudget.ts';
|
|
24
26
|
|
|
@@ -468,6 +470,16 @@ export interface FlatShaderOptions {
|
|
|
468
470
|
* when absent. See `modelsGlsl`.
|
|
469
471
|
*/
|
|
470
472
|
readonly physicalSpecular?: boolean;
|
|
473
|
+
/**
|
|
474
|
+
* Whether it carries a draw's surface overlay, `uOverlay` and all (`setSurfaceOverlay`). True
|
|
475
|
+
* when absent. See `overlayGlsl`.
|
|
476
|
+
*/
|
|
477
|
+
readonly surfaceOverlay?: boolean;
|
|
478
|
+
/**
|
|
479
|
+
* Whether the frame's own draws write the reflection maps beside their colour: WebGL2's way to
|
|
480
|
+
* the frame's materials' reflections. True when absent. See `reflectionSurfaceGlsl`.
|
|
481
|
+
*/
|
|
482
|
+
readonly reflectionMaps?: boolean;
|
|
471
483
|
/**
|
|
472
484
|
* How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
|
|
473
485
|
*
|
|
@@ -613,6 +625,10 @@ export function flatFrag(options: FlatShaderOptions): string {
|
|
|
613
625
|
SURFACE_EFFECTS_GLSL,
|
|
614
626
|
/* Depth zero and after the tangent frame and the lobes it calls; see models.ts. */
|
|
615
627
|
modelsGlsl(options.physicalSpecular ?? true),
|
|
628
|
+
/* After the models, so its switch takes the next id; depth zero, as main calls it. */
|
|
629
|
+
overlayGlsl(options.surfaceOverlay ?? true),
|
|
630
|
+
/* After the overlay, so its switch takes the next id. See reflectionSurface.ts. */
|
|
631
|
+
reflectionSurfaceGlsl(options.reflectionMaps ?? true),
|
|
616
632
|
/* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
|
|
617
633
|
AREA_LIGHT_GLSL,
|
|
618
634
|
MAIN_GLSL,
|
|
@@ -26,6 +26,10 @@ export const MAIN_GLSL = `void main() {
|
|
|
26
26
|
vec2 fxDx = dFdx(surfaceAt.xy);
|
|
27
27
|
vec2 fxDy = dFdy(surfaceAt.xy);
|
|
28
28
|
if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
|
|
29
|
+
/* A draw's dissolve cuts the surface away before anything is shaded: overlay.ts. */
|
|
30
|
+
if (SURFACE_OVERLAY) {
|
|
31
|
+
if (overlayCuts(surfaceAt.xy)) discard;
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
/*
|
|
31
35
|
* Surface colour and its coverage, resolved before anything else — including before it is
|
|
@@ -140,6 +144,11 @@ export const MAIN_GLSL = `void main() {
|
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
vec3 lit = albedo;
|
|
147
|
+
/* How much of the environment the frame shows here, and how a found reflection would land:
|
|
148
|
+
reflectionSurface.ts. None where nothing is reflected. */
|
|
149
|
+
vec3 reflectedProbe = vec3(0.0);
|
|
150
|
+
vec3 reflectedTint = vec3(0.0);
|
|
151
|
+
float reflectedRoughness = 1.0;
|
|
143
152
|
/* Skin's diffuse, held before emission for the screen-space blur's own pass: models.ts. */
|
|
144
153
|
vec3 diffuseAlone = vec3(0.0);
|
|
145
154
|
/* The light arriving at a glass pane from behind it, gathered in each light loop below and added
|
|
@@ -187,7 +196,10 @@ export const MAIN_GLSL = `void main() {
|
|
|
187
196
|
* right: a mipped colour image, sampled under uniform control flow, exactly as uAlbedo is
|
|
188
197
|
* below. The rule is about non-uniform branches, and this branch is on a uniform.
|
|
189
198
|
*/
|
|
199
|
+
vec3 unmapped = n;
|
|
190
200
|
if (uNormalStrength > 0.0) n = normalMapped(n, surfaceAt);
|
|
201
|
+
/* A draw's wrinkles, by region, over whatever the map made: overlay.ts. */
|
|
202
|
+
if (SURFACE_OVERLAY) n = overlayWrinkle(n, unmapped, surfaceAt.xy);
|
|
191
203
|
|
|
192
204
|
/*
|
|
193
205
|
* Microscopic relief: the surface's own texture, as a turn of the normal rather than a change
|
|
@@ -916,6 +928,15 @@ export const MAIN_GLSL = `void main() {
|
|
|
916
928
|
environment * mix(vec3(1.0), albedo, metal),
|
|
917
929
|
weight
|
|
918
930
|
);
|
|
931
|
+
reflectedTint = mix(vec3(1.0), albedo, metal) * weight;
|
|
932
|
+
reflectedProbe = environment * reflectedTint;
|
|
933
|
+
reflectedRoughness = surfaceRoughness;
|
|
934
|
+
}
|
|
935
|
+
/* The reflection pass's surface half leaves here, before the lamps: reflectionSurface.ts. */
|
|
936
|
+
if (REFLECTION_SURFACE) {
|
|
937
|
+
reflectionSurfaceOut(reflectedProbe, reflectedTint, reflectedRoughness, ormOcclusion,
|
|
938
|
+
reflectionAir());
|
|
939
|
+
return;
|
|
919
940
|
}
|
|
920
941
|
/*
|
|
921
942
|
* The sun's highlight, on top of whatever the surface reflects rather than underneath it —
|
|
@@ -1940,6 +1961,9 @@ export const MAIN_GLSL = `void main() {
|
|
|
1940
1961
|
* mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE) * fxGlow;
|
|
1941
1962
|
/* And a lit window's glow, which the scene's lit share already times: not gated on night. */
|
|
1942
1963
|
lit += windowGlow * uEmission.x;
|
|
1964
|
+
/* And a draw's overlay: its rim, its dissolve's band and the colour laid over. Not gated on the
|
|
1965
|
+
night, as emission is: a glow the surface wears rather than light it gives off. overlay.ts. */
|
|
1966
|
+
if (SURFACE_OVERLAY) lit += overlayGlow(surfaceAt.xy, n);
|
|
1943
1967
|
|
|
1944
1968
|
#if NIGHT_EMISSIVE
|
|
1945
1969
|
/*
|
|
@@ -2138,6 +2162,10 @@ export const MAIN_GLSL = `void main() {
|
|
|
2138
2162
|
return;
|
|
2139
2163
|
}
|
|
2140
2164
|
|
|
2165
|
+
/* WebGL2's frame writes the reflection maps itself, with the fog already reckoned above. */
|
|
2166
|
+
if (REFLECTION_MAPS) {
|
|
2167
|
+
reflectionSurfaceOut(reflectedProbe, reflectedTint, reflectedRoughness, ormOcclusion, fog);
|
|
2168
|
+
}
|
|
2141
2169
|
outColor = vec4(shaded, alpha);
|
|
2142
2170
|
}
|
|
2143
2171
|
`;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A draw's surface overlay in the lit stage: the rim, the dissolve and the wrinkles
|
|
3
|
+
* `surfaceOverlay.ts` packs into `uOverlay`. Called from `main` at three places — the dissolve's cut
|
|
4
|
+
* before anything is shaded, the wrinkles after the normal map, and the glow beside emission.
|
|
5
|
+
*
|
|
6
|
+
* **A lit switch, `SURFACE_OVERLAY`**, off until a draw first sets an overlay and then on for good,
|
|
7
|
+
* as `litSwitchesGlsl`'s are; declared after the models so every switch before it keeps its id.
|
|
8
|
+
* Compiled out, the three calls are stubs and the fifteen vectors are not declared at all, which is
|
|
9
|
+
* what lets WebGL2 build a lit program without them on a device that has no room.
|
|
10
|
+
*
|
|
11
|
+
* **Images come from the atlas bound where the frame's refraction copy goes**, which an opaque draw
|
|
12
|
+
* never reads; a draw that refracts or is glass reads that copy there, so its overlay reads no image
|
|
13
|
+
* (`overlayImagesBound`). Each image is read by `textureLod` at level 0: several are reached under a
|
|
14
|
+
* branch on a per-pixel value, and the atlas carries no chain anyway.
|
|
15
|
+
*/
|
|
16
|
+
export function overlayGlsl(on: boolean): string {
|
|
17
|
+
if (!on) {
|
|
18
|
+
return /* glsl */ `
|
|
19
|
+
const bool SURFACE_OVERLAY = false; // wgsl:override
|
|
20
|
+
bool overlayCuts(vec2 uv) { return false; }
|
|
21
|
+
vec3 overlayWrinkle(vec3 mapped, vec3 unmapped, vec2 uv) { return mapped; }
|
|
22
|
+
vec3 overlayGlow(vec2 uv, vec3 n) { return vec3(0.0); }
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
return /* glsl */ `
|
|
26
|
+
/* Whether this program carries a draw's overlay. Off but where one has been set: overlay.ts. */
|
|
27
|
+
const bool SURFACE_OVERLAY = true; // wgsl:override
|
|
28
|
+
/* The overlay's fifteen vectors; packSurfaceOverlay in surfaceOverlay.ts says what each holds. */
|
|
29
|
+
uniform vec4 uOverlay[15];
|
|
30
|
+
|
|
31
|
+
/* Whether the atlas is what the refraction slot holds: not where this draw refracts or is glass. */
|
|
32
|
+
bool overlayImagesBound() {
|
|
33
|
+
return uSeeThrough.x <= 0.0 && uSeeThrough.z <= 0.0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* One image of the atlas at uv, repeated within its region; zero where the region has no size. */
|
|
37
|
+
vec4 overlayImage(vec4 region, vec2 uv) {
|
|
38
|
+
if (region.x <= 0.0) return vec4(0.0);
|
|
39
|
+
return textureLod(uRefractScene, region.zw + fract(uv) * region.xy, 0.0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* The dissolve's noise at uv, and whether the draw dissolves at all: -1 where it does not. */
|
|
43
|
+
float overlayDissolveNoise(vec2 uv) {
|
|
44
|
+
if (uOverlay[6].x <= 0.0 || uOverlay[7].z <= 0.0) return -1.0;
|
|
45
|
+
if (!overlayImagesBound()) return -1.0;
|
|
46
|
+
return overlayImage(uOverlay[6], uv * uOverlay[7].xy).r;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Whether the dissolve cuts this fragment away: where its noise is under the threshold, or all of
|
|
50
|
+
it at a threshold of 1. */
|
|
51
|
+
bool overlayCuts(vec2 uv) {
|
|
52
|
+
float noise = overlayDissolveNoise(uv);
|
|
53
|
+
if (noise < 0.0) return false;
|
|
54
|
+
return noise < uOverlay[7].z || uOverlay[7].z >= 1.0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The wrinkle normal blended into the mapped one by the two masks' channels times the six weights,
|
|
59
|
+
* in the frame the normal map is read in. **Under uniform control flow up to the frame**, which
|
|
60
|
+
* takes derivatives: every branch before it is on the overlay's numbers alone, and the weight a
|
|
61
|
+
* pixel takes is a mix rather than a branch.
|
|
62
|
+
*/
|
|
63
|
+
vec3 overlayWrinkle(vec3 mapped, vec3 unmapped, vec2 uv) {
|
|
64
|
+
if (uOverlay[10].x <= 0.0 || !overlayImagesBound()) return mapped;
|
|
65
|
+
mat3 tbn = tangentFrame(unmapped, vWorldPos, vUv.xy, vTangent, vHasTangents);
|
|
66
|
+
vec3 a = overlayImage(uOverlay[11], uv).rgb;
|
|
67
|
+
vec3 b = overlayImage(uOverlay[12], uv).rgb;
|
|
68
|
+
float weight = clamp(
|
|
69
|
+
dot(a, uOverlay[13].xyz) + dot(b, vec3(uOverlay[13].w, uOverlay[14].xy)), 0.0, 1.0);
|
|
70
|
+
vec3 wrinkle = overlayImage(uOverlay[10], uv).xyz * 2.0 - 1.0;
|
|
71
|
+
/* z rebuilt where none is stored, as normalMapped does: see normalMap.ts. */
|
|
72
|
+
wrinkle.z = wrinkle.z > 0.0 ? wrinkle.z : sqrt(max(0.0, 1.0 - dot(wrinkle.xy, wrinkle.xy)));
|
|
73
|
+
return normalize(mix(mapped, normalize(tbn * wrinkle), weight));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* The rim's place on the screen, 0 to 1 up its height and its width in the same units: from view
|
|
78
|
+
* space and the projection's tangent, not from gl_FragCoord, whose y runs up on one backend and
|
|
79
|
+
* down on the other (the 2026-08-17 rule), and whose scale is a render target's, not the screen's.
|
|
80
|
+
*/
|
|
81
|
+
vec2 overlayScreen() {
|
|
82
|
+
vec4 eye = uView * vec4(vWorldPos, 1.0);
|
|
83
|
+
float depth = max(-eye.z, 1e-4) * uClusterFrustum.z;
|
|
84
|
+
return vec2(eye.x / depth + uClusterFrustum.w, eye.y / depth + 1.0) * 0.5;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* What the overlay adds to the light the surface gives off: the dissolve's band, the colour laid
|
|
88
|
+
over through its noise, and the rim. Zero where the draw sets none of them. */
|
|
89
|
+
vec3 overlayGlow(vec2 uv, vec3 n) {
|
|
90
|
+
vec3 glow = vec3(0.0);
|
|
91
|
+
float noise = overlayDissolveNoise(uv);
|
|
92
|
+
if (noise >= 0.0) {
|
|
93
|
+
float band = 1.0 - clamp((noise - uOverlay[7].z) / uOverlay[7].w, 0.0, 1.0);
|
|
94
|
+
glow += uOverlay[8].rgb * uOverlay[8].w * band + uOverlay[9].rgb * uOverlay[9].w * noise;
|
|
95
|
+
}
|
|
96
|
+
float strength = uOverlay[0].w;
|
|
97
|
+
if (strength > 0.0) {
|
|
98
|
+
vec3 toEye = normalize(uCameraPos - vWorldPos);
|
|
99
|
+
float edge = pow(1.0 - clamp(dot(n, toEye), 0.0, 1.0), max(uOverlay[1].x, 1e-4));
|
|
100
|
+
float up = pow(clamp(n.y * 0.5 + 0.5, 0.0, 1.0), max(uOverlay[1].y, 1e-4));
|
|
101
|
+
float rim = pow(edge * up, max(uOverlay[1].z, 1e-4));
|
|
102
|
+
float time = uSurfaceScene.x;
|
|
103
|
+
if (uOverlay[2].x > 0.0 && overlayImagesBound()) {
|
|
104
|
+
vec2 at = overlayScreen() * uOverlay[3].z + uOverlay[3].xy * time;
|
|
105
|
+
rim *= overlayImage(uOverlay[2], at).r;
|
|
106
|
+
}
|
|
107
|
+
rim = min(rim, 1.0);
|
|
108
|
+
rim *= mix(uOverlay[4].x, uOverlay[4].y, 0.5 + 0.5 * sin(uOverlay[3].w * time));
|
|
109
|
+
if (uOverlay[5].x > 0.0 && overlayImagesBound()) {
|
|
110
|
+
rim *= 1.0 - uOverlay[1].w * overlayImage(uOverlay[5], uv).r;
|
|
111
|
+
}
|
|
112
|
+
glow += uOverlay[0].rgb * strength * rim;
|
|
113
|
+
}
|
|
114
|
+
return glow;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lit stage leaves for a reflection traced in the frame: how much of the environment each
|
|
3
|
+
* pixel shows, and how a reflection found in the frame would land there instead. Read by the
|
|
4
|
+
* reflection pass (`screenSpaceReflection.ts`), which swaps the one for the other where its ray
|
|
5
|
+
* finds something.
|
|
6
|
+
*
|
|
7
|
+
* **The swap is exact because the lit stage's own blend is a mix**: `lit = mix(lit, environment ·
|
|
8
|
+
* tint, weight)` leaves `environment · tint · weight` of the probe in the frame, and a reflection
|
|
9
|
+
* found in the frame belongs there with the same tint and the same weight. So the frame gains
|
|
10
|
+
* `(found · tint − environment · tint) · weight` and nothing else moves: the diffuse the blend
|
|
11
|
+
* already took its share from keeps it. Both are carried the rest of the way the shaded colour
|
|
12
|
+
* goes — the occlusion, the water and the medium in front of the surface — so the swap lands in the
|
|
13
|
+
* frame's own units.
|
|
14
|
+
*
|
|
15
|
+
* **Two outputs beside the colour, written by every lit draw and kept only where a target is
|
|
16
|
+
* bound.** WebGL2 binds them during opaque lit draws alone; WebGPU's frame pass binds none, and a
|
|
17
|
+
* second pass replays the frame's opaque draws into them with `REFLECTION_SURFACE` on, which leaves
|
|
18
|
+
* before the lamps (`main`). What it gives up is a vector of arithmetic on every lit pixel whether
|
|
19
|
+
* or not anything reads it.
|
|
20
|
+
*/
|
|
21
|
+
export function reflectionSurfaceGlsl(maps: boolean): string {
|
|
22
|
+
return /* glsl */ `
|
|
23
|
+
/*
|
|
24
|
+
* Whether this pipeline is the reflection pass's surface half: the frame's opaque draws again,
|
|
25
|
+
* against the depth they wrote, leaving the two outputs below and no light. Off in every other
|
|
26
|
+
* pipeline, and set from the key as skin's halves are. See reflectionSurface.ts.
|
|
27
|
+
*/
|
|
28
|
+
const bool REFLECTION_SURFACE = false; // wgsl:override
|
|
29
|
+
/*
|
|
30
|
+
* Whether the frame's own draws write the two outputs as well: WebGL2's arrangement, on where the
|
|
31
|
+
* frame's materials' reflections are, and off in every WebGPU pipeline, which replays the draws
|
|
32
|
+
* instead. A lit switch, so a program that does not write them carries nothing live across the
|
|
33
|
+
* lamps for them.
|
|
34
|
+
*/
|
|
35
|
+
const bool REFLECTION_MAPS = ${maps ? 'true' : 'false'}; // wgsl:override
|
|
36
|
+
/* The environment as the frame shows it, and the surface's roughness in w. */
|
|
37
|
+
layout(location = 1) out vec4 outReflectedProbe;
|
|
38
|
+
/* What a reflection found in the frame is multiplied by to land where the environment did. */
|
|
39
|
+
layout(location = 2) out vec4 outReflectedTint;
|
|
40
|
+
|
|
41
|
+
/* How much of the surface the medium in front of it leaves: the fog block below's own measure. */
|
|
42
|
+
float reflectionAir() {
|
|
43
|
+
return uFogEnabled != 0 ? mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y) : 0.0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* The two outputs, carried through what the shaded colour meets after the blend: its occlusion,
|
|
48
|
+
* the water, and the air, as \`main\` applies them. \`air\` is the fog the caller has already
|
|
49
|
+
* reckoned, so the frame's own draws pay for it once.
|
|
50
|
+
*/
|
|
51
|
+
void reflectionSurfaceOut(vec3 probe, vec3 tint, float roughness, float occlusion, float air) {
|
|
52
|
+
vec3 keep = vec3(occlusion * (1.0 - air));
|
|
53
|
+
if (uFogEnabled != 0) {
|
|
54
|
+
vec3 waterTransmission = vec3(0.42) + uUnderwaterColor * 2.0;
|
|
55
|
+
keep *= mix(vec3(1.0), waterTransmission, uUnderwaterFactor * 0.55);
|
|
56
|
+
}
|
|
57
|
+
outReflectedProbe = vec4(probe * keep, roughness);
|
|
58
|
+
outReflectedTint = vec4(tint * keep, 1.0);
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
@@ -85,11 +85,18 @@ float mediumFog(float dist, float pointY) {
|
|
|
85
85
|
return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode
|
|
89
|
+
// never read before Atmosphere.fogStart. The ray is integrated from there on, at the density
|
|
90
|
+
// of that point; at a start of 0 every term is the one it always was. See render/fog.ts.
|
|
91
|
+
float start = min(uFogNear, dist);
|
|
92
|
+
float before = dist > 0.0 ? start / dist : 0.0;
|
|
88
93
|
float t = (pointY - uFogEyeY) * uFogHeightFalloff;
|
|
94
|
+
float rest = t * (1.0 - before);
|
|
89
95
|
// (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than
|
|
90
96
|
// branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.
|
|
91
|
-
float denom = abs(
|
|
92
|
-
float air =
|
|
97
|
+
float denom = abs(rest) < 1e-4 ? 1e-4 : rest;
|
|
98
|
+
float air =
|
|
99
|
+
1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);
|
|
93
100
|
if (uUnderwaterFactor <= 0.0) return air;
|
|
94
101
|
// Water is its own medium and keeps its own curve, so crossing the surface
|
|
95
102
|
// changes nothing about how the water column reads.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chromatic aberration: a lens bending each colour by its own amount, so red and green land a
|
|
3
|
+
* little nearer the centre than blue toward the frame's edges.
|
|
4
|
+
*
|
|
5
|
+
* **Linear in wavelength, from an offset outward.** A simple lens's dispersion is close to linear
|
|
6
|
+
* in wavelength over the visible range, so red (611 nm) and green (549 nm) are pulled toward the
|
|
7
|
+
* centre by their distance from blue (464 nm), which is drawn where it is. Nothing moves inside
|
|
8
|
+
* `start` — a share of the half-frame, 0 at the centre — and the pull grows linearly past it,
|
|
9
|
+
* scaled so that the frame's edge takes the whole of `intensity`, which is a percentage.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up**: it re-reads the scene at the composite's first sample, so the camera's
|
|
12
|
+
* motion blur and the depth of field, which re-read the scene themselves, carry less of the fringe
|
|
13
|
+
* where they are strong. A pass of its own after both would carry it whole, at a full-frame read
|
|
14
|
+
* and write a frame. **What would make that wrong** is a game that blurs heavily and fringes
|
|
15
|
+
* heavily at once, where the colour edges would be seen to soften with the blur.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** How many floats `resolveFringe` fills: the red pull, the green pull, and where they begin. */
|
|
19
|
+
export const FRINGE_FLOATS = 3;
|
|
20
|
+
|
|
21
|
+
/** The primaries' wavelengths, nanometres. */
|
|
22
|
+
const RED_NM = 611.3;
|
|
23
|
+
const GREEN_NM = 549.1;
|
|
24
|
+
const BLUE_NM = 464.3;
|
|
25
|
+
/** The lens's dispersion per nanometre, the constant that makes `intensity` a percentage. */
|
|
26
|
+
const PER_NM = 0.007;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The fringe's three numbers, into `out`: how far red and green are pulled toward the centre at
|
|
30
|
+
* the frame's edge, as a share of the half-frame, and the start offset. `intensity` 0 or below is
|
|
31
|
+
* none; a start of 1 or more is none too, since nothing then lies past it. Not finite is none.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveFringe(intensity: number, start: number, out: Float32Array): void {
|
|
34
|
+
const at = Number.isFinite(start) ? Math.min(Math.max(start, 0), 1) : 0;
|
|
35
|
+
const amount = Number.isFinite(intensity) && intensity > 0 && at < 1 ? intensity * 0.01 : 0;
|
|
36
|
+
/* Divided only where there is something to divide: at a start of 1 this is 0 over 0. */
|
|
37
|
+
const reach = amount > 0 ? amount / (1 - at) : 0;
|
|
38
|
+
out[0] = reach * PER_NM * (RED_NM - BLUE_NM);
|
|
39
|
+
out[1] = reach * PER_NM * (GREEN_NM - BLUE_NM);
|
|
40
|
+
out[2] = at;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `withFringe(scene)`: the scene's colour with red and green read again where the lens put them.
|
|
45
|
+
* Reads `uScene` and `vUv`, which the including stage declares, and `uFringe`, which this declares.
|
|
46
|
+
*/
|
|
47
|
+
export const FRINGE_GLSL = `
|
|
48
|
+
/** Red's and green's pull at the frame's edge, and where it begins: see shaders/fringe.ts. */
|
|
49
|
+
uniform vec3 uFringe;
|
|
50
|
+
|
|
51
|
+
vec3 withFringe(vec3 scene) {
|
|
52
|
+
if (uFringe.x <= 0.0) return scene;
|
|
53
|
+
vec2 lens = vUv * 2.0 - 1.0;
|
|
54
|
+
vec2 past = sign(lens) * clamp(abs(lens) - uFringe.z, 0.0, 1.0);
|
|
55
|
+
vec2 red = (lens - past * uFringe.x) * 0.5 + 0.5;
|
|
56
|
+
vec2 green = (lens - past * uFringe.y) * 0.5 + 0.5;
|
|
57
|
+
return vec3(textureLod(uScene, red, 0.0).r, textureLod(uScene, green, 0.0).g, scene.b);
|
|
58
|
+
}
|
|
59
|
+
`;
|