@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.
Files changed (163) hide show
  1. package/CHANGELOG.json +61 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +7 -0
  5. package/dist/render/atmosphere.d.ts +13 -0
  6. package/dist/render/atmosphere.js +9 -1
  7. package/dist/render/backend/api.d.ts +9 -1
  8. package/dist/render/backend/createRenderer.d.ts +6 -0
  9. package/dist/render/backend/createRenderer.js +4 -1
  10. package/dist/render/backend/webgl2/renderer.d.ts +90 -2
  11. package/dist/render/backend/webgl2/renderer.js +297 -16
  12. package/dist/render/backend/webgpu/device.d.ts +7 -1
  13. package/dist/render/backend/webgpu/device.js +31 -3
  14. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  15. package/dist/render/backend/webgpu/flatPass.js +58 -26
  16. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  17. package/dist/render/backend/webgpu/postPass.js +16 -2
  18. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  19. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +131 -2
  21. package/dist/render/backend/webgpu/renderer.js +586 -70
  22. package/dist/render/displayRange.d.ts +31 -0
  23. package/dist/render/displayRange.js +47 -0
  24. package/dist/render/filmicCurve.d.ts +74 -0
  25. package/dist/render/filmicCurve.js +203 -0
  26. package/dist/render/fog.d.ts +4 -1
  27. package/dist/render/fog.js +12 -2
  28. package/dist/render/frame/resources.d.ts +2 -2
  29. package/dist/render/frame/resources.js +6 -0
  30. package/dist/render/frameReflections.d.ts +40 -0
  31. package/dist/render/frameReflections.js +48 -0
  32. package/dist/render/pass.d.ts +49 -0
  33. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  34. package/dist/render/reflectionSurfaceTargets.js +99 -0
  35. package/dist/render/renderQuality.d.ts +39 -8
  36. package/dist/render/renderQuality.js +4 -0
  37. package/dist/render/sceneTarget.d.ts +5 -0
  38. package/dist/render/sceneTarget.js +24 -1
  39. package/dist/render/screenSpaceReflection.d.ts +5 -0
  40. package/dist/render/screenSpaceReflection.js +5 -0
  41. package/dist/render/shaders/arcane.d.ts +1 -1
  42. package/dist/render/shaders/bolt.d.ts +1 -1
  43. package/dist/render/shaders/caustics.d.ts +1 -1
  44. package/dist/render/shaders/film.d.ts +1 -1
  45. package/dist/render/shaders/filmLook.js +2 -1
  46. package/dist/render/shaders/fire.d.ts +1 -1
  47. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  48. package/dist/render/shaders/flat/glassTint.js +2 -0
  49. package/dist/render/shaders/flat/index.d.ts +10 -0
  50. package/dist/render/shaders/flat/index.js +6 -0
  51. package/dist/render/shaders/flat/main.d.ts +1 -1
  52. package/dist/render/shaders/flat/main.js +28 -0
  53. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  54. package/dist/render/shaders/flat/overlay.js +117 -0
  55. package/dist/render/shaders/flat/pointShadow.js +1 -1
  56. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  57. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  58. package/dist/render/shaders/fog.d.ts +1 -1
  59. package/dist/render/shaders/fog.js +9 -2
  60. package/dist/render/shaders/fringe.d.ts +29 -0
  61. package/dist/render/shaders/fringe.js +55 -0
  62. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  63. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  64. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  65. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  66. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  67. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  68. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  69. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  70. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  71. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  72. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -16
  73. package/dist/render/shaders/generated/flat.wgsl.js +214 -54
  74. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  75. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  76. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  77. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  78. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  79. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  80. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  81. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  82. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  83. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  84. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  85. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  86. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  87. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  88. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  89. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  90. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  91. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  92. package/dist/render/shaders/line.d.ts +1 -1
  93. package/dist/render/shaders/particle.d.ts +3 -3
  94. package/dist/render/shaders/rush.js +54 -11
  95. package/dist/render/shaders/scatter.d.ts +1 -1
  96. package/dist/render/shaders/smoke.d.ts +1 -1
  97. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  98. package/dist/render/shaders/ssrResolve.js +51 -0
  99. package/dist/render/shaders/ssrTrace.js +26 -0
  100. package/dist/render/shaders/water.d.ts +1 -1
  101. package/dist/render/ssrPass.d.ts +16 -0
  102. package/dist/render/ssrPass.js +106 -1
  103. package/dist/render/surfaceOverlay.d.ts +132 -0
  104. package/dist/render/surfaceOverlay.js +85 -0
  105. package/dist/render/surfaceTexture.d.ts +19 -0
  106. package/dist/render/surfaceTexture.js +69 -0
  107. package/dist/render/temporalAa.d.ts +14 -0
  108. package/dist/render/temporalAa.js +20 -0
  109. package/dist/render/vertexDefaults.d.ts +17 -0
  110. package/dist/render/vertexDefaults.js +24 -0
  111. package/package.json +6 -6
  112. package/src/index.ts +16 -0
  113. package/src/render/atmosphere.ts +22 -1
  114. package/src/render/backend/api.ts +9 -0
  115. package/src/render/backend/createRenderer.ts +18 -1
  116. package/src/render/backend/webgl2/renderer.ts +357 -13
  117. package/src/render/backend/webgpu/device.ts +42 -3
  118. package/src/render/backend/webgpu/flatPass.ts +63 -26
  119. package/src/render/backend/webgpu/postPass.ts +18 -0
  120. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  121. package/src/render/backend/webgpu/renderer.ts +735 -83
  122. package/src/render/displayRange.ts +61 -0
  123. package/src/render/filmicCurve.ts +229 -0
  124. package/src/render/fog.ts +19 -3
  125. package/src/render/frame/resources.ts +6 -0
  126. package/src/render/frameReflections.ts +72 -0
  127. package/src/render/pass.ts +53 -1
  128. package/src/render/reflectionSurfaceTargets.ts +105 -0
  129. package/src/render/renderQuality.ts +44 -8
  130. package/src/render/sceneTarget.ts +30 -2
  131. package/src/render/screenSpaceReflection.ts +5 -0
  132. package/src/render/shaders/filmLook.ts +2 -1
  133. package/src/render/shaders/flat/glassTint.ts +5 -1
  134. package/src/render/shaders/flat/index.ts +16 -0
  135. package/src/render/shaders/flat/main.ts +28 -0
  136. package/src/render/shaders/flat/overlay.ts +117 -0
  137. package/src/render/shaders/flat/pointShadow.ts +1 -1
  138. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  139. package/src/render/shaders/fog.ts +9 -2
  140. package/src/render/shaders/fringe.ts +59 -0
  141. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  142. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  143. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  144. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  145. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  146. package/src/render/shaders/generated/flat.wgsl.ts +214 -54
  147. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  148. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  149. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  150. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  151. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  152. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  153. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  154. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  155. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  156. package/src/render/shaders/rush.ts +54 -11
  157. package/src/render/shaders/ssrResolve.ts +52 -0
  158. package/src/render/shaders/ssrTrace.ts +26 -0
  159. package/src/render/ssrPass.ts +138 -1
  160. package/src/render/surfaceOverlay.ts +195 -0
  161. package/src/render/surfaceTexture.ts +67 -0
  162. package/src/render/temporalAa.ts +20 -0
  163. package/src/render/vertexDefaults.ts +28 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which dynamic range the frame goes out in, and why: the one decision behind
3
+ * `RenderQuality.highDynamicRange`, made here so both backends answer it in the same words.
4
+ *
5
+ * **A high range is asked for, found, and held, or it is not had.** Three things must all be true:
6
+ * the profile asked; the frame has a composite holding light past white (`screenEffects` and
7
+ * `hdrScene`), since every other pass writes display values already clipped; and the display says
8
+ * it can show more than paper white. The backend then has to be able to configure such a canvas,
9
+ * which only it knows, and it may still answer standard after this says high.
10
+ *
11
+ * **What a high range gives up** is nothing a standard frame had: paper white stays where it is,
12
+ * and only what was above it — a lamp, a flash, a sunlit edge — goes on rising to the display's
13
+ * peak instead of rolling off at white. **What would make it wrong** is a display that reports a
14
+ * high range it cannot show, which a page cannot detect and the platform owns.
15
+ */
16
+
17
+ export type DisplayRange = 'high' | 'standard';
18
+
19
+ export interface DisplayRangeChoice {
20
+ readonly range: DisplayRange;
21
+ /** A sentence saying why, for `RendererApi.displayRangeReason`. */
22
+ readonly reason: string;
23
+ }
24
+
25
+ /** The display's own answer, as the browser gives it. A host without a DOM supplies its own. */
26
+ export function browserHighDynamicRange(): boolean {
27
+ // platform: browser default — `CreateRendererOptions.highDynamicRangeDisplay` replaces it
28
+ return typeof matchMedia === 'function' && matchMedia('(dynamic-range: high)').matches;
29
+ }
30
+
31
+ /** The range a profile gets on a display, before the backend has tried to configure it. */
32
+ export function chooseDisplayRange(
33
+ asked: boolean,
34
+ composite: boolean,
35
+ displayIsHigh: boolean,
36
+ ): DisplayRangeChoice {
37
+ if (!asked) return { range: 'standard', reason: 'highDynamicRange was not asked for' };
38
+ if (!composite) {
39
+ return {
40
+ range: 'standard',
41
+ reason:
42
+ 'highDynamicRange needs screenEffects and hdrScene: without a composite holding light ' +
43
+ 'past white, every pass writes values already clipped to it',
44
+ };
45
+ }
46
+ if (!displayIsHigh) {
47
+ return { range: 'standard', reason: 'the display reports a standard dynamic range' };
48
+ }
49
+ return { range: 'high', reason: 'an extended-range canvas on a high dynamic range display' };
50
+ }
51
+
52
+ /**
53
+ * The display's peak over the frame's paper white, as `setDisplayLuminance` is handed it: at
54
+ * least 1, and 1 where the range is standard or a number is not one. Capped at 64, a thousand nits
55
+ * over a sixteen-nit white, which no display reaches and no curve should be asked to.
56
+ */
57
+ export function displayHeadroom(range: DisplayRange, paperWhite: number, peak: number): number {
58
+ if (range !== 'high') return 1;
59
+ const ratio = peak / paperWhite;
60
+ return Number.isFinite(ratio) ? Math.min(Math.max(ratio, 1), 64) : 1;
61
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The `filmic` output transform: a parametric film curve, its five numbers, and the eight
3
+ * constants the resolve reads, decided once here for both backends.
4
+ *
5
+ * **Why a second filmic curve beside `aces`.** `aces` is the reference RRT and ODT fit, which
6
+ * holds a mid grey of 0.18 at about 0.11 of display linear: graded for a dark cinema and kept dark.
7
+ * The curve here holds 0.18 at 0.18 by construction, and is shaped by five numbers a grading
8
+ * volume carries — a slope through mid grey, a toe and a shoulder, and how far black and white are
9
+ * clipped past the ends — so a look authored against such a curve is matched by passing its numbers
10
+ * rather than by baking the difference into a colour lookup table, which has the fewest steps
11
+ * exactly where two curves differ most: in the shadows.
12
+ *
13
+ * **What the curve is.** In the ACEScg working space, after the ACES reference's glow and red
14
+ * modifier and a 4% desaturation: a straight line of `slope` through mid grey in log10, joined to a
15
+ * logistic toe below `toeMatch` and a logistic shoulder above `shoulderMatch`, blended by a smooth
16
+ * step between the two, then desaturated by 7% and taken back to linear sRGB. The toe's position is
17
+ * solved so that 0.18 in is 0.18 out.
18
+ *
19
+ * **What it gives up.** It needs the composite to grade: `screenEffects` and `hdrScene`, since the
20
+ * forward passes that grade a frame without one carry two uniforms between them and not eight.
21
+ * Without them every pass grades with `aces` and the renderer says so once at construction.
22
+ * **What would make it wrong** is a profile wanting this curve without a scene target, which would
23
+ * then pay six floats in every forward program to have it.
24
+ */
25
+
26
+ /** A film curve's five numbers, as a grading volume states them. */
27
+ export interface FilmicCurve {
28
+ /** The straight segment's slope in log10, through mid grey. Positive. */
29
+ readonly slope: number;
30
+ /** How much of the curve below mid grey the toe takes, 0 to 1. Larger is a longer, softer toe. */
31
+ readonly toe: number;
32
+ /** How much of the curve above mid grey the shoulder takes, 0 to 1. Larger rolls off sooner. */
33
+ readonly shoulder: number;
34
+ /** How far below 0 the toe's asymptote sits: 0 holds black at black, more crushes it. */
35
+ readonly blackClip: number;
36
+ /** How far above 1 the shoulder's asymptote sits: 0 never reaches white, more lets it clip. */
37
+ readonly whiteClip: number;
38
+ }
39
+
40
+ /** The defaults the curve ships with where a volume states nothing. */
41
+ export const DEFAULT_FILMIC_CURVE: FilmicCurve = {
42
+ slope: 0.88,
43
+ toe: 0.55,
44
+ shoulder: 0.26,
45
+ blackClip: 0,
46
+ whiteClip: 0.04,
47
+ };
48
+
49
+ /** How many floats `resolveFilmicCurve` fills: two vectors of the resolve's uniforms. */
50
+ export const FILMIC_CONSTANTS = 8;
51
+
52
+ /** Mid grey, which the curve takes to itself. */
53
+ const MID_GREY = 0.18;
54
+
55
+ /** The largest toe or shoulder kept: at 1 the toe's or the shoulder's scale is zero. */
56
+ const MOST = 0.999;
57
+ /** The shallowest slope kept, which is still a curve rather than a flat line. */
58
+ const LEAST_SLOPE = 0.01;
59
+
60
+ /**
61
+ * The eight constants the resolve reads, into `out`: `uFilmA` = (slope, black clip, white clip,
62
+ * toe scale) and `uFilmB` = (shoulder scale, toe match, straight match, shoulder match). Returns
63
+ * whether any of the five numbers had to be brought into range.
64
+ *
65
+ * Here rather than in the shader, because they are one solve a curve and would be one per pixel
66
+ * there. **Clamped rather than refused**, because a grading volume is set from inside a frame and
67
+ * the frame loop never throws: a slope below 0.01, a toe, shoulder or clip outside 0 to 1, and a
68
+ * toe or shoulder of exactly 1 — whose scale is then zero and divides the curve by it — are each
69
+ * a picture made of NaN otherwise. A number that is not finite takes the default's. The renderer
70
+ * says once when anything was clamped.
71
+ */
72
+ export function resolveFilmicCurve(
73
+ curve: FilmicCurve,
74
+ out: Float32Array,
75
+ /**
76
+ * The display's peak over paper white (`displayHeadroom`), 1 for a standard range. Above 1 the
77
+ * white clip is raised by the difference, so the shoulder rolls off toward the display's peak
78
+ * rather than toward white, and the toe and mid grey stay exactly where they were.
79
+ */
80
+ headroom = 1,
81
+ ): boolean {
82
+ let clamped = false;
83
+ const within = (value: number, low: number, high: number, fallback: number): number => {
84
+ const finite = Number.isFinite(value) ? value : fallback;
85
+ const kept = Math.min(Math.max(finite, low), high);
86
+ if (kept !== value) clamped = true;
87
+ return kept;
88
+ };
89
+ const slope = within(curve.slope, LEAST_SLOPE, Number.MAX_VALUE, DEFAULT_FILMIC_CURVE.slope);
90
+ const toe = within(curve.toe, 0, MOST, DEFAULT_FILMIC_CURVE.toe);
91
+ const shoulder = within(curve.shoulder, 0, MOST, DEFAULT_FILMIC_CURVE.shoulder);
92
+ const blackClip = within(curve.blackClip, 0, 1, DEFAULT_FILMIC_CURVE.blackClip);
93
+ const whiteClip =
94
+ within(curve.whiteClip, 0, 1, DEFAULT_FILMIC_CURVE.whiteClip) + Math.max(headroom, 1) - 1;
95
+ const toeScale = 1 + blackClip - toe;
96
+ const shoulderScale = 1 + whiteClip - shoulder;
97
+ let toeMatch: number;
98
+ if (toe > 0.8) {
99
+ /* Mid grey falls on the straight segment. */
100
+ toeMatch = (1 - toe - MID_GREY) / slope + Math.log10(MID_GREY);
101
+ } else {
102
+ /* Mid grey falls on the toe: solve the toe's logistic for 0.18 in, 0.18 out. */
103
+ const bt = (MID_GREY + blackClip) / toeScale - 1;
104
+ toeMatch = Math.log10(MID_GREY) - 0.5 * Math.log((1 + bt) / (1 - bt)) * (toeScale / slope);
105
+ }
106
+ const straightMatch = (1 - toe) / slope - toeMatch;
107
+ const shoulderMatch = shoulder / slope - straightMatch;
108
+ out[0] = slope;
109
+ out[1] = blackClip;
110
+ out[2] = whiteClip;
111
+ out[3] = toeScale;
112
+ out[4] = shoulderScale;
113
+ out[5] = toeMatch;
114
+ out[6] = straightMatch;
115
+ out[7] = shoulderMatch;
116
+ return clamped;
117
+ }
118
+
119
+ /** Said once a renderer, the first time `setFilmicCurve` was handed a number out of range. */
120
+ export const FILMIC_CLAMPED =
121
+ '[driftengine] setFilmicCurve was handed a number outside its range — a slope under 0.01, a ' +
122
+ 'toe, shoulder or clip outside 0 to 1, or one that is not a number — and drew with it clamped.';
123
+
124
+ /**
125
+ * Said once at construction where `filmic` was asked for and the frame has no composite that
126
+ * grades: the passes grade with `aces`, which is the nearest curve they carry.
127
+ */
128
+ export const FILMIC_WITHOUT_COMPOSITE =
129
+ "[driftengine] outputTransform 'filmic' is applied by the composite, which grades only with " +
130
+ "`screenEffects` and `hdrScene` both on: without them every pass grades itself, with 'aces'.";
131
+
132
+ /**
133
+ * `filmicCurve(c)`: linear sRGB in, display-linear sRGB out, clamped to 0 to 1 — or, on a high
134
+ * range display, to the shoulder's own asymptote. Reads `uFilmA`, `uFilmB` and `uDisplayHeadroom`,
135
+ * which the including stage declares. The ACES helpers are the reference's own:
136
+ * saturation, `yc`, the glow's sigmoid, and hue in degrees.
137
+ */
138
+ export const FILMIC_GLSL = `
139
+ /* sRGB to ACEScg (through D60), ACEScg back, and ACEScg to and from ACES2065-1. Columns. */
140
+ const mat3 FILM_SRGB_TO_AP1 = mat3(
141
+ 0.61319, 0.07021, 0.02062,
142
+ 0.33951, 0.91634, 0.10957,
143
+ 0.04737, 0.01345, 0.86961
144
+ );
145
+ /* The inverse of the one above, computed rather than copied, so a round trip is the identity. */
146
+ const mat3 FILM_AP1_TO_SRGB = mat3(
147
+ 1.70480, -0.13027, -0.02401,
148
+ -0.62168, 1.14082, -0.12900,
149
+ -0.08325, -0.01055, 1.15324
150
+ );
151
+ const mat3 FILM_AP1_TO_AP0 = mat3(
152
+ 0.6954522414, 0.0447945634, -0.0055258826,
153
+ 0.1406786965, 0.8596711185, 0.0040252103,
154
+ 0.1638690622, 0.0955343182, 1.0015006723
155
+ );
156
+ const mat3 FILM_AP0_TO_AP1 = mat3(
157
+ 1.4514393161, -0.0765537734, 0.0083161484,
158
+ -0.2365107469, 1.1762296998, -0.0060324498,
159
+ -0.2149285693, -0.0996759264, 0.9977163014
160
+ );
161
+ const vec3 FILM_AP1_LUMA = vec3(0.2722287168, 0.6740817658, 0.0536895174);
162
+
163
+ float filmSaturation(vec3 c) {
164
+ float hi = max(c.r, max(c.g, c.b));
165
+ float lo = min(c.r, min(c.g, c.b));
166
+ return (max(hi, 1e-10) - max(lo, 1e-10)) / max(hi, 1e-2);
167
+ }
168
+
169
+ float filmYc(vec3 c) {
170
+ float chroma = sqrt(max(c.b * (c.b - c.g) + c.g * (c.g - c.r) + c.r * (c.r - c.b), 0.0));
171
+ return (c.b + c.g + c.r + 1.75 * chroma) / 3.0;
172
+ }
173
+
174
+ float filmSigmoid(float x) {
175
+ float t = max(1.0 - abs(x / 2.0), 0.0);
176
+ return (1.0 + sign(x) * (1.0 - t * t)) / 2.0;
177
+ }
178
+
179
+ float filmGlow(float yc, float gain, float mid) {
180
+ if (yc <= 2.0 / 3.0 * mid) return gain;
181
+ if (yc >= 2.0 * mid) return 0.0;
182
+ return gain * (mid / yc - 0.5);
183
+ }
184
+
185
+ /* Hue in degrees, -180 to 180 about red; 0 for a grey, which then takes no red modifier. */
186
+ float filmCentredHue(vec3 c) {
187
+ float y = sqrt(3.0) * (c.g - c.b);
188
+ float x = 2.0 * c.r - c.g - c.b;
189
+ if (abs(x) < 1e-10 && abs(y) < 1e-10) return 0.0;
190
+ return degrees(atan(y, x));
191
+ }
192
+
193
+ vec3 filmicCurve(vec3 linear) {
194
+ vec3 ap0 = FILM_AP1_TO_AP0 * (FILM_SRGB_TO_AP1 * linear);
195
+ /* The reference's glow, lifting dark saturated colours a little. */
196
+ float saturation = filmSaturation(ap0);
197
+ ap0 *= 1.0 + filmGlow(filmYc(ap0), 0.05 * filmSigmoid((saturation - 0.4) / 0.2), 0.08);
198
+ /* And its red modifier, pulling saturated reds toward a pivot so they do not run to orange. */
199
+ float weight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * filmCentredHue(ap0) / 135.0));
200
+ ap0.r += weight * weight * saturation * (0.03 - ap0.r) * (1.0 - 0.82);
201
+ vec3 work = max(FILM_AP0_TO_AP1 * ap0, vec3(0.0));
202
+ work = mix(vec3(dot(work, FILM_AP1_LUMA)), work, 0.96);
203
+
204
+ float slope = uFilmA.x;
205
+ float black = uFilmA.y;
206
+ float white = uFilmA.z;
207
+ float toeScale = uFilmA.w;
208
+ float shoulderScale = uFilmB.x;
209
+ float toeMatch = uFilmB.y;
210
+ float straightMatch = uFilmB.z;
211
+ float shoulderMatch = uFilmB.w;
212
+ /* log10, through log2: GLSL ES has no log10. */
213
+ vec3 logc = log2(max(work, vec3(1e-10))) * 0.30102999566;
214
+ vec3 straight = slope * (logc + straightMatch);
215
+ vec3 toeC = -black + (2.0 * toeScale) / (1.0 + exp((-2.0 * slope / toeScale) * (logc - toeMatch)));
216
+ vec3 shoulderC =
217
+ (1.0 + white) - (2.0 * shoulderScale) / (1.0 + exp((2.0 * slope / shoulderScale) * (logc - shoulderMatch)));
218
+ toeC = mix(straight, toeC, vec3(lessThan(logc, vec3(toeMatch))));
219
+ shoulderC = mix(straight, shoulderC, vec3(greaterThan(logc, vec3(shoulderMatch))));
220
+ vec3 t = clamp((logc - toeMatch) / (shoulderMatch - toeMatch), 0.0, 1.0);
221
+ if (shoulderMatch < toeMatch) t = 1.0 - t;
222
+ t = (3.0 - 2.0 * t) * t * t;
223
+ vec3 tone = mix(toeC, shoulderC, t);
224
+ tone = mix(vec3(dot(tone, FILM_AP1_LUMA)), tone, 0.93);
225
+ /* White is the ceiling on a standard display; on a high range one the shoulder's asymptote is. */
226
+ float ceiling = uDisplayHeadroom > 1.0 ? 1.0 + white : 1.0;
227
+ return clamp(FILM_AP1_TO_SRGB * max(tone, vec3(0.0)), 0.0, ceiling);
228
+ }
229
+ `;
package/src/render/fog.ts CHANGED
@@ -40,7 +40,10 @@ export interface FogOptions {
40
40
  readonly underwaterFactor: number;
41
41
  /** `MEDIUM_FOG` integrates a medium; `LINEAR_FOG` ramps between near and far. */
42
42
  readonly mode: number;
43
- /** Linear mode only: where haze begins, and where it is total. */
43
+ /**
44
+ * Where haze begins: the ramp's start in linear mode, and in the medium the distance along the
45
+ * ray it is integrated from (`Atmosphere.fogStart`), 0 at the camera.
46
+ */
44
47
  readonly near: number;
45
48
  readonly far: number;
46
49
  }
@@ -88,13 +91,26 @@ export function mediumFog(fog: FogOptions, dist: number, pointY: number): number
88
91
  return mix(ramp, 1 - Math.pow(2, -wetLinear * wetLinear * 1.442695), fog.underwaterFactor);
89
92
  }
90
93
 
94
+ /*
95
+ * **Where the medium begins along the ray**, `near` — the linear mode's slot, which this mode
96
+ * never read before `Atmosphere.fogStart`. The ray is integrated from that point on: the density
97
+ * there, carried over what is left of the ray. At a start of 0 every term below is the one it
98
+ * always was, so a world that sets none draws the same bits.
99
+ */
100
+ const start = Math.min(fog.near, dist);
101
+ const before = dist > 0 ? start / dist : 0;
91
102
  const t = (pointY - fog.eyeY) * fog.heightFalloff;
103
+ const rest = t * (1 - before);
92
104
  /*
93
105
  * `(1 - e^-t)/t`, which tends to 1 as the ray levels out. **Guarded rather than branched**: at
94
106
  * `t = 0` the quotient is 0/0, and 1e-4 of a scale height is 8 mm.
95
107
  */
96
- const denom = Math.abs(t) < 1e-4 ? 1e-4 : t;
97
- const air = 1 - Math.exp((-fog.density * dist * (1 - Math.exp(-denom))) / denom);
108
+ const denom = Math.abs(rest) < 1e-4 ? 1e-4 : rest;
109
+ const air =
110
+ 1 -
111
+ Math.exp(
112
+ (-fog.density * Math.exp(-t * before) * (dist - start) * (1 - Math.exp(-denom))) / denom,
113
+ );
98
114
  if (fog.underwaterFactor <= 0) return air;
99
115
  /* Water is its own medium and keeps its own curve, so crossing the surface changes nothing
100
116
  about how the water column reads. */
@@ -30,6 +30,12 @@ export const FRAME_RESOURCES = [
30
30
  'aoTarget',
31
31
  'depthSnapshot',
32
32
  'inset',
33
+ /*
34
+ * The frame's colour as a registered pass reads it: a copy taken at that pass's draw, so reading
35
+ * it constrains no attachment's store, as `depthSnapshot` does not. Last, so no bit before it
36
+ * moved when it arrived.
37
+ */
38
+ 'colorSnapshot',
33
39
  ] as const;
34
40
 
35
41
  export type FrameResource = (typeof FRAME_RESOURCES)[number];
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Reflections traced through the frame's own materials: `RenderQuality.screenSpaceReflections`.
3
+ *
4
+ * **Every opaque lit surface reflects the frame by its own material**, where `ReflectiveSurface`
5
+ * reflects it by a box's: the lit stage leaves, per pixel, how much of the environment it showed and
6
+ * at what roughness (`shaders/flat/reflectionSurface.ts`), and a ray marched against the frame's
7
+ * depth swaps that share for what it finds — so a glossy panel mirrors the figures on it and the
8
+ * matte panel beside it barely does, in one mesh with one material map. Past `maxRoughness` a
9
+ * surface keeps the probes' reflection alone, which is what a march cannot do better than: a rough
10
+ * lobe wants many rays, and one ray blurred is only a fair stand-in for a narrow one.
11
+ *
12
+ * **What a surface reflects is what the lit stage already reflected**: a metal by its metalness, a
13
+ * dielectric where `setSurfaceReflectivity` or its material asks for one. A surface that reflects
14
+ * no environment reflects no frame either, which is what keeps the two from disagreeing.
15
+ */
16
+
17
+ /** How the frame's materials reflect it. Every field optional; see `DEFAULT_FRAME_REFLECTIONS`. */
18
+ export interface FrameReflections {
19
+ /** The roughness past which a surface reflects the probes alone, faded over the last quarter. */
20
+ readonly maxRoughness?: number;
21
+ /** How far a ray travels before it gives up, in metres. */
22
+ readonly reachM?: number;
23
+ /** How deep a depth sample is believed to be, in metres. See `ScreenSpaceMarch.thicknessM`. */
24
+ readonly thicknessM?: number;
25
+ /** Samples along a ray, at most 32. */
26
+ readonly steps?: number;
27
+ /** How far a fully rough reflection is smeared, as a share of the frame's height. */
28
+ readonly blur?: number;
29
+ }
30
+
31
+ export type ResolvedFrameReflections = Required<FrameReflections>;
32
+
33
+ /** The march `ReflectiveSurface` defaults to, a ceiling at satin, and a smear of 3% of the frame. */
34
+ export const DEFAULT_FRAME_REFLECTIONS: ResolvedFrameReflections = Object.freeze({
35
+ maxRoughness: 0.6,
36
+ reachM: 8,
37
+ thicknessM: 0.25,
38
+ steps: 24,
39
+ blur: 0.03,
40
+ });
41
+
42
+ const finite = (value: number | undefined, fallback: number): number =>
43
+ value !== undefined && Number.isFinite(value) ? value : fallback;
44
+
45
+ /**
46
+ * The option as the renderers read it: null where it is off, and every number held where the
47
+ * shader can use it — a ceiling above zero, a reach and a thickness above zero, a whole number of
48
+ * steps from 1 to 32, and a blur from none to a fifth of the frame. Clamped rather than refused,
49
+ * since a quality option is read at construction and a renderer that threw there would draw nothing.
50
+ */
51
+ export function resolveFrameReflections(
52
+ asked: boolean | FrameReflections | undefined,
53
+ ): ResolvedFrameReflections | null {
54
+ if (asked === undefined || asked === false) return null;
55
+ const given = asked === true ? {} : asked;
56
+ const d = DEFAULT_FRAME_REFLECTIONS;
57
+ return {
58
+ maxRoughness: Math.min(Math.max(finite(given.maxRoughness, d.maxRoughness), 0.01), 1),
59
+ reachM: Math.max(finite(given.reachM, d.reachM), 0.01),
60
+ thicknessM: Math.max(finite(given.thicknessM, d.thicknessM), 0.001),
61
+ steps: Math.min(Math.max(Math.round(finite(given.steps, d.steps)), 1), 32),
62
+ blur: Math.min(Math.max(finite(given.blur, d.blur), 0), 0.2),
63
+ };
64
+ }
65
+
66
+ /** What a renderer says once where the frame cannot carry these reflections, and why. */
67
+ export function frameReflectionsRefused(reason: string): string {
68
+ return (
69
+ `screenSpaceReflections is not drawn: ${reason}. Surfaces keep the probes' reflection; ` +
70
+ 'the trace needs `screenEffects` and `hdrScene`, and a frame of one sample.'
71
+ );
72
+ }
@@ -119,7 +119,23 @@ export type PassContext = {
119
119
  /** The exposure that goes with it. 1 where the composite will apply the real one. */
120
120
  readonly outputExposure: number;
121
121
  } & (
122
- | { readonly backend: 'webgl2'; readonly gl: WebGL2RenderingContext }
122
+ | {
123
+ readonly backend: 'webgl2';
124
+ readonly gl: WebGL2RenderingContext;
125
+ /**
126
+ * The frame's colour with everything drawn before this pass, for a pass declaring
127
+ * `reads: ['colorSnapshot']`, and null for one that does not or where the frame keeps no
128
+ * scene target (`screenEffects` off). A copy, never the attachment being drawn into, which
129
+ * WebGL2 cannot sample; taken afresh at each such pass's draw. WebGPU hands the same copy
130
+ * over in `PrepareContext.sceneColor`, where a bind group has to be built before the pass.
131
+ */
132
+ readonly sceneColor: WebGLTexture | null;
133
+ /**
134
+ * The frame's depth as it stands, for a pass declaring `reads: ['depthSnapshot']`: a copy, at
135
+ * the buffer's own values and reversed as it is (1 near, 0 far). Null as `sceneColor` is.
136
+ */
137
+ readonly sceneDepth: WebGLTexture | null;
138
+ }
123
139
  | {
124
140
  readonly backend: 'webgpu';
125
141
  readonly pass: GPURenderPassEncoder;
@@ -217,6 +233,42 @@ export type PrepareContext =
217
233
  * should not apply it either; the renderer's own verbs do not jitter a mirror.
218
234
  */
219
235
  readonly jitter: Float32Array;
236
+ /**
237
+ * The frame's single-sample copy of its depth, or null where the frame keeps none (no
238
+ * composite: `screenEffects` off).
239
+ *
240
+ * One `r32float` texel a pixel holding the depth buffer's own value, reversed as the
241
+ * buffer is (`REVERSED_DEPTH`: 1 at the near plane, 0 at the far one). Unfilterable, so a
242
+ * pass binds it as `'unfilterable-float'` and reads it with a non-filtering sampler or
243
+ * `textureLoad`.
244
+ *
245
+ * **Handed over here, filled at the draw.** The view is the one this frame's copy will be
246
+ * written into, so a pass builds its bind groups with it before the frame's pass opens; the
247
+ * copy itself is taken when a pass that declares `reads: ['depthSnapshot']` is drawn, with
248
+ * everything drawn before it, which is what the soft edge of a translucent particle
249
+ * needs. A pass that does not declare that read finds whatever the frame last wrote there.
250
+ */
251
+ readonly sceneDepth: GPUTextureView | null;
252
+ /**
253
+ * The frame's colour, for a pass that declares `reads: ['colorSnapshot']`, or null where no
254
+ * registered pass does or the frame keeps no composite (`screenEffects` off).
255
+ *
256
+ * At the scene's own size and format — under a reconstruction, the render's rather than the
257
+ * output's, since that is the picture a registered pass draws into — and filterable.
258
+ * **Handed over here, refilled at the draw**: the view is the texture each reading pass's
259
+ * draw copies the frame into, with everything drawn before it, so a distortion samples the
260
+ * scene behind it and a full-screen effect drawn last reads the whole frame and writes over
261
+ * it, in scene light, before the tone curve. A copy a reading pass, where `sceneDepth` is one
262
+ * a frame.
263
+ *
264
+ * **Under a reconstruction a pass that reads it is drawn after the upscale**, over the
265
+ * reconstructed picture and after every blended draw, whatever order it was asked in — at the
266
+ * output's size and the reconstruction's format, which `PassContext.format` names at that
267
+ * draw, so a pass drawn there keeps a pipeline for it. The view is then the output-size copy.
268
+ * `sceneDepth` stays the render's opaque depth: at a ratio above 1 its texels are coarser by
269
+ * the ratio, and a blended draw is in neither depth, as it never is.
270
+ */
271
+ readonly sceneColor: GPUTextureView | null;
220
272
  };
221
273
 
222
274
  /**
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The frame's materials' reflection maps on WebGL2: two half-float textures on the scene's own
3
+ * framebuffer, beside its colour, written by the opaque lit draws as they are drawn. What the two
4
+ * hold is `shaders/flat/reflectionSurface.ts`'s to say; this is where they live on this backend.
5
+ *
6
+ * **Written in the frame's own pass, where WebGPU replays the draws in a second one.** A WebGL2
7
+ * program writing an output its framebuffer has no buffer enabled for loses that output and nothing
8
+ * else, so the maps are enabled around an opaque lit draw (`on`, `off`) and stay disabled for every
9
+ * other: the sky, glass, text and particles write no material, and a draw into a mirror, a probe or
10
+ * a capture is into another framebuffer, whose draw buffers this never touches.
11
+ *
12
+ * **Detached once the trace has read them** (`detach`), because the resolve then draws into the
13
+ * scene's framebuffer while sampling both, and a texture sampled while attached to the framebuffer
14
+ * being drawn is a feedback loop whether or not its buffer is enabled. `begin` attaches them again.
15
+ */
16
+ const ZERO = new Float32Array(4);
17
+
18
+ export class ReflectionSurfaceTargets {
19
+ private probe: WebGLTexture | null = null;
20
+ private tint: WebGLTexture | null = null;
21
+ private width = 0;
22
+ private height = 0;
23
+ private readonly all: number[];
24
+ private readonly colourAlone: number[];
25
+
26
+ constructor(private readonly gl: WebGL2RenderingContext) {
27
+ this.all = [gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2];
28
+ this.colourAlone = [gl.COLOR_ATTACHMENT0];
29
+ }
30
+
31
+ /** The environment as the frame shows it, and the roughness in alpha. */
32
+ get probeTexture(): WebGLTexture | null {
33
+ return this.probe;
34
+ }
35
+
36
+ /** What a found reflection is multiplied by. */
37
+ get tintTexture(): WebGLTexture | null {
38
+ return this.tint;
39
+ }
40
+
41
+ /**
42
+ * At the frame's start, with the scene's framebuffer bound: both at its size, attached, cleared to
43
+ * nothing reflected, and left disabled.
44
+ */
45
+ begin(width: number, height: number): void {
46
+ const { gl } = this;
47
+ if (this.probe === null || this.width !== width || this.height !== height) {
48
+ this.release();
49
+ this.probe = this.make(width, height);
50
+ this.tint = this.make(width, height);
51
+ this.width = width;
52
+ this.height = height;
53
+ }
54
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, this.probe, 0);
55
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, this.tint, 0);
56
+ gl.drawBuffers(this.all);
57
+ gl.clearBufferfv(gl.COLOR, 1, ZERO);
58
+ gl.clearBufferfv(gl.COLOR, 2, ZERO);
59
+ gl.drawBuffers(this.colourAlone);
60
+ }
61
+
62
+ /** Around an opaque lit draw into the frame: its two extra outputs land. */
63
+ on(): void {
64
+ this.gl.drawBuffers(this.all);
65
+ }
66
+
67
+ /** And after it: only the colour does. */
68
+ off(): void {
69
+ this.gl.drawBuffers(this.colourAlone);
70
+ }
71
+
72
+ /** With the scene's framebuffer bound, after the trace has read them: see the header. */
73
+ detach(): void {
74
+ const { gl } = this;
75
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, null, 0);
76
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT2, gl.TEXTURE_2D, null, 0);
77
+ }
78
+
79
+ dispose(): void {
80
+ this.release();
81
+ }
82
+
83
+ private make(width: number, height: number): WebGLTexture {
84
+ const { gl } = this;
85
+ const texture = gl.createTexture();
86
+ if (texture === null) throw new Error('ReflectionSurfaceTargets: createTexture failed');
87
+ /* Unit 0, chosen rather than inherited, for the reason `SsrPass` gives. */
88
+ gl.activeTexture(gl.TEXTURE0);
89
+ gl.bindTexture(gl.TEXTURE_2D, texture);
90
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA16F, width, height);
91
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
92
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
93
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
94
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
95
+ gl.bindTexture(gl.TEXTURE_2D, null);
96
+ return texture;
97
+ }
98
+
99
+ private release(): void {
100
+ if (this.probe !== null) this.gl.deleteTexture(this.probe);
101
+ if (this.tint !== null) this.gl.deleteTexture(this.tint);
102
+ this.probe = null;
103
+ this.tint = null;
104
+ }
105
+ }