@yali-engine/render 0.0.0-stage → 0.4.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/dist/bvh.d.ts +66 -0
- package/dist/bvh.js +493 -0
- package/dist/bvh.js.map +1 -0
- package/dist/camera.d.ts +34 -0
- package/dist/camera.js +66 -0
- package/dist/camera.js.map +1 -0
- package/dist/cluster-build.d.ts +46 -0
- package/dist/cluster-build.js +179 -0
- package/dist/cluster-build.js.map +1 -0
- package/dist/cluster-pages.d.ts +70 -0
- package/dist/cluster-pages.js +203 -0
- package/dist/cluster-pages.js.map +1 -0
- package/dist/cluster-stream.d.ts +83 -0
- package/dist/cluster-stream.js +226 -0
- package/dist/cluster-stream.js.map +1 -0
- package/dist/culled-draws.d.ts +30 -0
- package/dist/culled-draws.js +103 -0
- package/dist/culled-draws.js.map +1 -0
- package/dist/debug-lines.d.ts +51 -0
- package/dist/debug-lines.js +158 -0
- package/dist/debug-lines.js.map +1 -0
- package/dist/device-profile.d.ts +100 -0
- package/dist/device-profile.js +131 -0
- package/dist/device-profile.js.map +1 -0
- package/dist/device.d.ts +39 -0
- package/dist/device.js +79 -0
- package/dist/device.js.map +1 -0
- package/dist/environment.d.ts +60 -0
- package/dist/environment.js +204 -0
- package/dist/environment.js.map +1 -0
- package/dist/extract.d.ts +74 -0
- package/dist/extract.js +194 -0
- package/dist/extract.js.map +1 -0
- package/dist/frame-graph.d.ts +112 -0
- package/dist/frame-graph.js +226 -0
- package/dist/frame-graph.js.map +1 -0
- package/dist/gpu-health.d.ts +74 -0
- package/dist/gpu-health.js +106 -0
- package/dist/gpu-health.js.map +1 -0
- package/dist/gpu-timer.d.ts +26 -0
- package/dist/gpu-timer.js +93 -0
- package/dist/gpu-timer.js.map +1 -0
- package/dist/headless.d.ts +28 -0
- package/dist/headless.js +91 -0
- package/dist/headless.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/lights.d.ts +81 -0
- package/dist/lights.js +247 -0
- package/dist/lights.js.map +1 -0
- package/dist/matrix.d.ts +6 -0
- package/dist/matrix.js +59 -0
- package/dist/matrix.js.map +1 -0
- package/dist/meshes.d.ts +411 -0
- package/dist/meshes.js +545 -0
- package/dist/meshes.js.map +1 -0
- package/dist/meshlets-decoder.d.ts +20 -0
- package/dist/meshlets-decoder.js +47 -0
- package/dist/meshlets-decoder.js.map +1 -0
- package/dist/meshlets.d.ts +186 -0
- package/dist/meshlets.js +1040 -0
- package/dist/meshlets.js.map +1 -0
- package/dist/occlusion.d.ts +51 -0
- package/dist/occlusion.js +188 -0
- package/dist/occlusion.js.map +1 -0
- package/dist/pick-decoder.d.ts +19 -0
- package/dist/pick-decoder.js +45 -0
- package/dist/pick-decoder.js.map +1 -0
- package/dist/pick.d.ts +86 -0
- package/dist/pick.js +452 -0
- package/dist/pick.js.map +1 -0
- package/dist/pipeline-cache.d.ts +53 -0
- package/dist/pipeline-cache.js +96 -0
- package/dist/pipeline-cache.js.map +1 -0
- package/dist/post-options.d.ts +109 -0
- package/dist/post-options.js +23 -0
- package/dist/post-options.js.map +1 -0
- package/dist/post-shaders.gen.d.ts +40 -0
- package/dist/post-shaders.gen.js +43 -0
- package/dist/post-shaders.gen.js.map +1 -0
- package/dist/post.d.ts +101 -0
- package/dist/post.js +555 -0
- package/dist/post.js.map +1 -0
- package/dist/render-plugin.d.ts +239 -0
- package/dist/render-plugin.js +633 -0
- package/dist/render-plugin.js.map +1 -0
- package/dist/scene.d.ts +406 -0
- package/dist/scene.js +2005 -0
- package/dist/scene.js.map +1 -0
- package/dist/shaders.gen.d.ts +40 -0
- package/dist/shaders.gen.js +43 -0
- package/dist/shaders.gen.js.map +1 -0
- package/dist/shadows.d.ts +104 -0
- package/dist/shadows.js +227 -0
- package/dist/shadows.js.map +1 -0
- package/dist/staging-ring.d.ts +41 -0
- package/dist/staging-ring.js +125 -0
- package/dist/staging-ring.js.map +1 -0
- package/dist/textures.d.ts +165 -0
- package/dist/textures.js +361 -0
- package/dist/textures.js.map +1 -0
- package/dist/tonemap.d.ts +45 -0
- package/dist/tonemap.js +141 -0
- package/dist/tonemap.js.map +1 -0
- package/dist/viewport.d.ts +78 -0
- package/dist/viewport.js +130 -0
- package/dist/viewport.js.map +1 -0
- package/dist/webgl2/plugin.d.ts +40 -0
- package/dist/webgl2/plugin.js +190 -0
- package/dist/webgl2/plugin.js.map +1 -0
- package/dist/webgl2/program.d.ts +27 -0
- package/dist/webgl2/program.js +74 -0
- package/dist/webgl2/program.js.map +1 -0
- package/dist/webgl2/renderer.d.ts +128 -0
- package/dist/webgl2/renderer.js +976 -0
- package/dist/webgl2/renderer.js.map +1 -0
- package/dist/webgl2/shaders.glsl.gen.d.ts +27 -0
- package/dist/webgl2/shaders.glsl.gen.js +20 -0
- package/dist/webgl2/shaders.glsl.gen.js.map +1 -0
- package/dist/webgl2/textures.d.ts +11 -0
- package/dist/webgl2/textures.js +82 -0
- package/dist/webgl2/textures.js.map +1 -0
- package/dist/webgl2/webgl2.d.ts +1 -0
- package/dist/webgl2/webgl2.js +4 -0
- package/dist/webgl2/webgl2.js.map +1 -0
- package/dist/worker-mode.d.ts +85 -0
- package/dist/worker-mode.js +204 -0
- package/dist/worker-mode.js.map +1 -0
- package/package.json +47 -3
- package/README.md +0 -3
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
import { YaliError } from '@yali-engine/core';
|
|
2
|
+
import { DEFAULT_SKY, ENV_MIPS, ENV_SIZE, mergeSky, packSkyParams, skySh9 } from "../environment.js";
|
|
3
|
+
import { CAMERA_BYTES, CULLED_BYTES, PALETTE_WIDTH } from "../scene.js";
|
|
4
|
+
import { binLights, CLUSTER_ROW, CLUSTERS, LightPacker } from "../lights.js";
|
|
5
|
+
import { isBlendClass, isSkinnedClass, RIGID_CLASSES, SKIN_STREAM_BYTES } from "../meshes.js";
|
|
6
|
+
import { CASCADE_UNIFORM_BYTES, cascadeCullPlanes, fitCascades, SHADOW_FLOATS, sweptPlanes } from "../shadows.js";
|
|
7
|
+
import { CULL_CASCADES, CulledDraws } from "../culled-draws.js";
|
|
8
|
+
import { CURVES as CURVE_IDS, TONE_PARAMS_BYTES, toneMapColor, writeToneParams } from "../tonemap.js";
|
|
9
|
+
import { DEBUG_VERTEX_BYTES } from "../debug-lines.js";
|
|
10
|
+
import { GlProgram } from "./program.js";
|
|
11
|
+
import { glDraw, glDrawBlend, glDrawFlat, glDrawFlatBlend, glDrawFlatMask, glDrawLambert, glDrawLambertBlend, glDrawLambertMask, glDrawMask, glPrefilter, glShadow, glSky, glTonemap, glLines, glVsShadowSkinned, glVsSkinned, } from "./shaders.glsl.gen.js";
|
|
12
|
+
/** The mask and blend programs of each shading level (ADR-0081); double-sided classes share them. */
|
|
13
|
+
const ALPHA_PROGRAMS = { mask: [glDrawMask, glDrawLambertMask, glDrawFlatMask], blend: [glDrawBlend, glDrawLambertBlend, glDrawFlatBlend] };
|
|
14
|
+
const VERTEX_BYTES = 32;
|
|
15
|
+
/** Drawing order (as `scene.ts`): rigid then skinned opaque and mask, then the blended ones. */
|
|
16
|
+
const CLASS_ORDER = [0, 1, 2, 3, 6, 7, 8, 9, 4, 5, 10, 11];
|
|
17
|
+
const CLEAR = [0.035, 0.035, 0.045, 1];
|
|
18
|
+
const CURVES = CURVE_IDS;
|
|
19
|
+
/** Bytes of one sky-pass uniform slot (a face and mip). */
|
|
20
|
+
const SKY_PARAMS_BYTES = 96;
|
|
21
|
+
/**
|
|
22
|
+
* The WebGL2 backend's renderer (M2 increment 8, ADR-0070): the WebGPU renderer's frame on
|
|
23
|
+
* GLSL ES 3.00 that naga translated from the same WGSL (`tools/glsl-gen.mjs`). The CPU culls
|
|
24
|
+
* and picks LODs (the extract's `runCulled`, ADR-0057) and bins the lights (`binLights`,
|
|
25
|
+
* identical to the GPU pass); the GPU draws:
|
|
26
|
+
*
|
|
27
|
+
* 1. the sun's shadow cascades into one depth atlas (`fitCascades`, the same fit);
|
|
28
|
+
* 2. the scene into a half-float HDR target, lit by the sun, the binned lights and the
|
|
29
|
+
* environment cube (built here face by face with the same sky shaders);
|
|
30
|
+
* 3. the tone curve into an RGBA8 target, blitted to the canvas.
|
|
31
|
+
*
|
|
32
|
+
* Every offscreen pass renders with naga's clip-space adjustment (y flipped, z to −1..1), so
|
|
33
|
+
* render targets keep WebGPU's row order and fragment coordinates (row 0 at the top) and the
|
|
34
|
+
* shaders need no change; the flip reverses triangle winding, so front faces are clockwise
|
|
35
|
+
* here, and the final blit to the canvas flips back. WebGL2 has no base vertex and no base
|
|
36
|
+
* instance, so each draw re-points the attributes at its mesh's vertices and its instances.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Whether this GPU ignores LINEAR filtering on depth comparisons (GLES allows it: the Galaxy
|
|
40
|
+
* F04's PowerVR returns 0 or 1, never a blend, so shadow edges step by a texel). One 1×1
|
|
41
|
+
* draw at startup: a 2×1 depth texture [0, 1] compared at its middle gives 0.5 when filtered.
|
|
42
|
+
*/
|
|
43
|
+
export function comparesNearestOnly(gl) {
|
|
44
|
+
const compile = (type, src) => {
|
|
45
|
+
const sh = gl.createShader(type);
|
|
46
|
+
gl.shaderSource(sh, src);
|
|
47
|
+
gl.compileShader(sh);
|
|
48
|
+
return sh;
|
|
49
|
+
};
|
|
50
|
+
const vs = compile(gl.VERTEX_SHADER, '#version 300 es\nvoid main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); gl_PointSize = 1.0; }');
|
|
51
|
+
const fs = compile(gl.FRAGMENT_SHADER, '#version 300 es\nprecision highp float;\nprecision highp sampler2DShadow;\nuniform sampler2DShadow t;\nout vec4 o;\nvoid main() { o = vec4(texture(t, vec3(0.5, 0.5, 0.5))); }');
|
|
52
|
+
const program = gl.createProgram();
|
|
53
|
+
gl.attachShader(program, vs);
|
|
54
|
+
gl.attachShader(program, fs);
|
|
55
|
+
gl.linkProgram(program);
|
|
56
|
+
const tex = gl.createTexture();
|
|
57
|
+
const target = gl.createTexture();
|
|
58
|
+
const fbo = gl.createFramebuffer();
|
|
59
|
+
let nearest = false;
|
|
60
|
+
try {
|
|
61
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
62
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
63
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
|
|
64
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, 2, 1, 0, gl.DEPTH_COMPONENT, gl.FLOAT, new Float32Array([0, 1]));
|
|
65
|
+
for (const [k, v] of [[gl.TEXTURE_MIN_FILTER, gl.LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR], [gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE], [gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE], [gl.TEXTURE_COMPARE_FUNC, gl.GEQUAL]])
|
|
66
|
+
gl.texParameteri(gl.TEXTURE_2D, k, v);
|
|
67
|
+
gl.bindSampler(0, null);
|
|
68
|
+
gl.bindTexture(gl.TEXTURE_2D, target);
|
|
69
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1);
|
|
70
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
71
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target, 0);
|
|
72
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
73
|
+
gl.viewport(0, 0, 1, 1);
|
|
74
|
+
gl.useProgram(program);
|
|
75
|
+
gl.uniform1i(gl.getUniformLocation(program, 't'), 0);
|
|
76
|
+
gl.bindVertexArray(null);
|
|
77
|
+
gl.drawArrays(gl.POINTS, 0, 1);
|
|
78
|
+
const px = new Uint8Array(4);
|
|
79
|
+
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
|
80
|
+
// Filtered: about 128. Nearest: 0 or 255.
|
|
81
|
+
nearest = px[0] < 64 || px[0] > 192;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
85
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
86
|
+
gl.useProgram(null);
|
|
87
|
+
for (const o of [tex, target])
|
|
88
|
+
gl.deleteTexture(o);
|
|
89
|
+
gl.deleteFramebuffer(fbo);
|
|
90
|
+
gl.deleteProgram(program);
|
|
91
|
+
gl.deleteShader(vs);
|
|
92
|
+
gl.deleteShader(fs);
|
|
93
|
+
}
|
|
94
|
+
return nearest;
|
|
95
|
+
}
|
|
96
|
+
export class GlRenderer {
|
|
97
|
+
gl;
|
|
98
|
+
// What `SceneControls` exposes (the WebGPU SceneRenderer's knobs that apply here).
|
|
99
|
+
cull = 'cpu';
|
|
100
|
+
path = 'vertex';
|
|
101
|
+
useBundles = false;
|
|
102
|
+
staging = null;
|
|
103
|
+
staticUploads = 0;
|
|
104
|
+
/** Instance bytes uploaded last frame (every visible record: the CPU cull rewrites them all). */
|
|
105
|
+
transformBytes = 0;
|
|
106
|
+
shaderError = '';
|
|
107
|
+
capacity;
|
|
108
|
+
count = 0;
|
|
109
|
+
visible = Number.NaN;
|
|
110
|
+
errorPx = 1;
|
|
111
|
+
lodBias = 0;
|
|
112
|
+
minRadiusPx = 0;
|
|
113
|
+
stats = { slots: 0, draws: 0, layouts: 0, meshes: 0, shaderBuilds: 0 };
|
|
114
|
+
/** The shading level (`SceneControls.shadingLevel`, ADR-0072). */
|
|
115
|
+
shadingLevel = 0;
|
|
116
|
+
shadingLevels;
|
|
117
|
+
lighting = { sunColor: [1, 1, 1], sunIntensity: 3, ambient: 1 };
|
|
118
|
+
/** Tone mapping (`ToneMapper`'s knobs). */
|
|
119
|
+
exposure = 1;
|
|
120
|
+
curve = 'neutral';
|
|
121
|
+
/** Colour grading in the tone-mapping pass (ADR-0092), or null. */
|
|
122
|
+
grading = null;
|
|
123
|
+
shadows;
|
|
124
|
+
/** The packed lights (`render.lights.count`, `dropped`). */
|
|
125
|
+
lights = new LightPacker();
|
|
126
|
+
/** Whether the HDR and environment targets are half-float (else RGBA8: highlights clip). */
|
|
127
|
+
hdr;
|
|
128
|
+
/**
|
|
129
|
+
* The draw tone-maps inline and renders straight into the RGBA8 target (T0's default,
|
|
130
|
+
* ADR-0071): no HDR target, no tone-mapping pass.
|
|
131
|
+
*/
|
|
132
|
+
inlineTone;
|
|
133
|
+
#store;
|
|
134
|
+
#materials;
|
|
135
|
+
#textures;
|
|
136
|
+
#programs;
|
|
137
|
+
/** The draw program of each shading level (index 0 is `#programs.draw`). */
|
|
138
|
+
#levelPrograms;
|
|
139
|
+
/** Mask and blend programs by `alpha mode - 1` and level, compiled the first time a mesh needs one. */
|
|
140
|
+
#alphaPrograms = [[], []];
|
|
141
|
+
/** The skinned draw programs (ADR-0088) by `alpha mode × 3 + level`, and the skinned shadow program. */
|
|
142
|
+
#skinnedPrograms = [];
|
|
143
|
+
#skinnedShadow = null;
|
|
144
|
+
/** The draw program of draw class `cls` at shading level `level`. */
|
|
145
|
+
#programFor(cls, level) {
|
|
146
|
+
const alpha = (cls % RIGID_CLASSES) >> 1;
|
|
147
|
+
const rigid = alpha === 0 ? this.#levelPrograms[level] : this.#alphaProgram(alpha, level);
|
|
148
|
+
if (!isSkinnedClass(cls))
|
|
149
|
+
return rigid;
|
|
150
|
+
// The skinned vertex stage with the rigid program's fragment stage (`tools/glsl-gen.mjs`).
|
|
151
|
+
const k = alpha * 3 + level;
|
|
152
|
+
let p = this.#skinnedPrograms[k];
|
|
153
|
+
if (!p) {
|
|
154
|
+
const src = alpha === 0 ? [glDraw, glDrawLambert, glDrawFlat][level] : ALPHA_PROGRAMS[alpha === 1 ? 'mask' : 'blend'][level];
|
|
155
|
+
p = new GlProgram(this.gl, `draw:skinned:${alpha}:${level}`, { vs: glVsSkinned, fs: src.fs });
|
|
156
|
+
this.#skinnedPrograms[k] = p;
|
|
157
|
+
}
|
|
158
|
+
return p;
|
|
159
|
+
}
|
|
160
|
+
#alphaProgram(alpha, level) {
|
|
161
|
+
const slot = this.#alphaPrograms[alpha - 1];
|
|
162
|
+
let p = slot[level];
|
|
163
|
+
if (!p) {
|
|
164
|
+
p = new GlProgram(this.gl, `draw:${alpha === 1 ? 'mask' : 'blend'}:${level}`, ALPHA_PROGRAMS[alpha === 1 ? 'mask' : 'blend'][level]);
|
|
165
|
+
slot[level] = p;
|
|
166
|
+
}
|
|
167
|
+
return p;
|
|
168
|
+
}
|
|
169
|
+
#ubo;
|
|
170
|
+
#uboAlign;
|
|
171
|
+
#samplers;
|
|
172
|
+
#vertexBuf;
|
|
173
|
+
#indexBuf;
|
|
174
|
+
#vertexBytes = 0;
|
|
175
|
+
#indexBytes = 0;
|
|
176
|
+
#vertexBlocks = 0;
|
|
177
|
+
#indexBlocks = 0;
|
|
178
|
+
/** The skinned pool (ADR-0088): its vertices and skin stream, made when the first skinned mesh arrives. */
|
|
179
|
+
#skinned = null;
|
|
180
|
+
/** The palette texture (`PALETTE_WIDTH` × rows, rgba32float) and its height in rows. */
|
|
181
|
+
#palette;
|
|
182
|
+
#paletteRows = 1;
|
|
183
|
+
/** Where the skinned draws read their palette each frame (`@yali-engine/animation` sets it). */
|
|
184
|
+
paletteSource = null;
|
|
185
|
+
#geometryVersion = -1;
|
|
186
|
+
#instanceBuf;
|
|
187
|
+
#instanceBytes = 0;
|
|
188
|
+
#drawVao;
|
|
189
|
+
#shadowVao;
|
|
190
|
+
#emptyVao;
|
|
191
|
+
#draws = new Uint32Array(0);
|
|
192
|
+
#drawCount = 0;
|
|
193
|
+
#byLod = new Uint32Array(8);
|
|
194
|
+
/** The CPU cull's draws, per slot and per cascade run (ADR-0073). */
|
|
195
|
+
#culled = new CulledDraws();
|
|
196
|
+
#cascadesCulled = 0;
|
|
197
|
+
#materialVersion = -1;
|
|
198
|
+
#lit = new Float64Array(7).fill(Number.NaN);
|
|
199
|
+
#clear = [...CLEAR];
|
|
200
|
+
#envVersion = 0;
|
|
201
|
+
#litEnvVersion = -1;
|
|
202
|
+
#lightingData = new Float32Array(48);
|
|
203
|
+
#cameraData = new Float32Array(CAMERA_BYTES / 4);
|
|
204
|
+
#shadowParams = new Float32Array(SHADOW_FLOATS);
|
|
205
|
+
/** The GPU compares depth nearest only, even with LINEAR filtering (`comparesNearestOnly`). */
|
|
206
|
+
#manualPcf;
|
|
207
|
+
#cascadeData;
|
|
208
|
+
#bins = new Uint32Array(CLUSTERS * CLUSTER_ROW);
|
|
209
|
+
#binCounts = new Uint32Array(CLUSTERS);
|
|
210
|
+
#binsTex;
|
|
211
|
+
/** Lights binned last frame (with none now and then, the bins are already all zero). */
|
|
212
|
+
#binnedLights = 0;
|
|
213
|
+
#toneData = new ArrayBuffer(TONE_PARAMS_BYTES);
|
|
214
|
+
// The environment.
|
|
215
|
+
#sky = { ...DEFAULT_SKY };
|
|
216
|
+
#envBase;
|
|
217
|
+
#envCube;
|
|
218
|
+
#sh9 = new Float32Array(36);
|
|
219
|
+
// Targets, (re)made at the viewport's size.
|
|
220
|
+
#width = 0;
|
|
221
|
+
#height = 0;
|
|
222
|
+
#hdrTex = null;
|
|
223
|
+
#depthRb = null;
|
|
224
|
+
#ldrRb = null;
|
|
225
|
+
#sceneFbo;
|
|
226
|
+
#ldrFbo;
|
|
227
|
+
#shadowFbo;
|
|
228
|
+
#shadowAtlas;
|
|
229
|
+
#envFbo;
|
|
230
|
+
#envReadFbo;
|
|
231
|
+
#captures = [];
|
|
232
|
+
#drawBindings;
|
|
233
|
+
constructor(gl, store, materials, textures, capacity, shadows, hdrTarget = true, shadingLevels = 1) {
|
|
234
|
+
this.#manualPcf = shadows !== null && comparesNearestOnly(gl);
|
|
235
|
+
this.gl = gl;
|
|
236
|
+
this.#store = store;
|
|
237
|
+
this.#materials = materials;
|
|
238
|
+
this.#textures = textures;
|
|
239
|
+
this.capacity = capacity;
|
|
240
|
+
this.hdr = !!(gl.getExtension('EXT_color_buffer_float') || gl.getExtension('EXT_color_buffer_half_float'));
|
|
241
|
+
this.inlineTone = !hdrTarget;
|
|
242
|
+
const maxSide = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
243
|
+
this.shadows = shadows && shadows.cascades > 0 ? { ...shadows, size: Math.min(shadows.size, Math.floor(maxSide / shadows.cascades)) } : null;
|
|
244
|
+
if (maxSide < CLUSTERS)
|
|
245
|
+
throw new YaliError('GL_LIMITS', `This WebGL2 context's largest texture is ${maxSide} texels; the light bins need ${CLUSTERS}.`, 'This device is below the WebGL2 backend\'s floor.');
|
|
246
|
+
this.#programs = {
|
|
247
|
+
draw: new GlProgram(gl, 'draw', glDraw),
|
|
248
|
+
shadow: new GlProgram(gl, 'shadow', glShadow),
|
|
249
|
+
sky: new GlProgram(gl, 'sky', glSky),
|
|
250
|
+
prefilter: new GlProgram(gl, 'prefilter', glPrefilter),
|
|
251
|
+
tonemap: new GlProgram(gl, 'tonemap', glTonemap),
|
|
252
|
+
};
|
|
253
|
+
this.shadingLevels = Math.max(1, Math.min(3, shadingLevels));
|
|
254
|
+
this.#levelPrograms = [this.#programs.draw, ...[glDrawLambert, glDrawFlat].slice(0, this.shadingLevels - 1).map((p, i) => new GlProgram(gl, `draw:${i + 1}`, p))];
|
|
255
|
+
this.#uboAlign = Math.max(16, gl.getParameter(gl.UNIFORM_BUFFER_OFFSET_ALIGNMENT));
|
|
256
|
+
const ubo = (bytes) => {
|
|
257
|
+
const b = gl.createBuffer();
|
|
258
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, b);
|
|
259
|
+
gl.bufferData(gl.UNIFORM_BUFFER, bytes, gl.DYNAMIC_DRAW);
|
|
260
|
+
return b;
|
|
261
|
+
};
|
|
262
|
+
const slot = (bytes) => Math.ceil(bytes / this.#uboAlign) * this.#uboAlign;
|
|
263
|
+
this.#cascadeData = new Float32Array((slot(CASCADE_UNIFORM_BYTES) * 4) / 4);
|
|
264
|
+
this.#ubo = {
|
|
265
|
+
camera: ubo(CAMERA_BYTES),
|
|
266
|
+
materials: ubo(materials.data.byteLength),
|
|
267
|
+
materialExt: ubo(materials.ext.byteLength),
|
|
268
|
+
lighting: ubo(192),
|
|
269
|
+
lights: ubo(this.lights.lights.byteLength),
|
|
270
|
+
cluster: ubo(this.lights.params.byteLength),
|
|
271
|
+
shadow: ubo(this.#shadowParams.byteLength),
|
|
272
|
+
cascades: ubo(slot(CASCADE_UNIFORM_BYTES) * 4),
|
|
273
|
+
sky: ubo(slot(SKY_PARAMS_BYTES) * 6 * ENV_MIPS),
|
|
274
|
+
tone: ubo(TONE_PARAMS_BYTES),
|
|
275
|
+
};
|
|
276
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.shadow);
|
|
277
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.#shadowParams); // no cascades until the first fit
|
|
278
|
+
const sampler = (params) => {
|
|
279
|
+
const s = gl.createSampler();
|
|
280
|
+
for (const [k, v] of params)
|
|
281
|
+
gl.samplerParameteri(s, k, v);
|
|
282
|
+
return s;
|
|
283
|
+
};
|
|
284
|
+
this.#samplers = {
|
|
285
|
+
material: sampler([[gl.TEXTURE_WRAP_S, gl.REPEAT], [gl.TEXTURE_WRAP_T, gl.REPEAT], [gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR]]),
|
|
286
|
+
linear: sampler([[gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE], [gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR]]),
|
|
287
|
+
compare: sampler([
|
|
288
|
+
[gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE], [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE], [gl.TEXTURE_MIN_FILTER, gl.LINEAR], [gl.TEXTURE_MAG_FILTER, gl.LINEAR],
|
|
289
|
+
// Lit when the fragment's depth is at least the stored one (reverse-Z).
|
|
290
|
+
[gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE], [gl.TEXTURE_COMPARE_FUNC, gl.GEQUAL],
|
|
291
|
+
]),
|
|
292
|
+
};
|
|
293
|
+
const aniso = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
294
|
+
if (aniso)
|
|
295
|
+
gl.samplerParameterf(this.#samplers.material, aniso.TEXTURE_MAX_ANISOTROPY_EXT, 4);
|
|
296
|
+
this.#vertexBuf = gl.createBuffer();
|
|
297
|
+
this.#indexBuf = gl.createBuffer();
|
|
298
|
+
this.#instanceBuf = gl.createBuffer();
|
|
299
|
+
this.#drawVao = gl.createVertexArray();
|
|
300
|
+
this.#shadowVao = gl.createVertexArray();
|
|
301
|
+
this.#emptyVao = gl.createVertexArray();
|
|
302
|
+
for (const vao of [this.#drawVao, this.#shadowVao]) {
|
|
303
|
+
gl.bindVertexArray(vao);
|
|
304
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#indexBuf);
|
|
305
|
+
gl.enableVertexAttribArray(0);
|
|
306
|
+
for (const loc of [2, 3]) {
|
|
307
|
+
gl.enableVertexAttribArray(loc);
|
|
308
|
+
gl.vertexAttribDivisor(loc, 1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
gl.bindVertexArray(this.#drawVao);
|
|
312
|
+
for (const loc of [1, 5])
|
|
313
|
+
gl.enableVertexAttribArray(loc);
|
|
314
|
+
gl.enableVertexAttribArray(4);
|
|
315
|
+
gl.vertexAttribDivisor(4, 1);
|
|
316
|
+
gl.bindVertexArray(null);
|
|
317
|
+
// The light bins: count and indices per cluster row, read with texelFetch.
|
|
318
|
+
this.#binsTex = gl.createTexture();
|
|
319
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#binsTex);
|
|
320
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R32UI, CLUSTER_ROW, CLUSTERS);
|
|
321
|
+
for (const [k, v] of [[gl.TEXTURE_MIN_FILTER, gl.NEAREST], [gl.TEXTURE_MAG_FILTER, gl.NEAREST]])
|
|
322
|
+
gl.texParameteri(gl.TEXTURE_2D, k, v);
|
|
323
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
|
|
324
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, CLUSTER_ROW, CLUSTERS, gl.RED_INTEGER, gl.UNSIGNED_INT, this.#bins);
|
|
325
|
+
// The shadow atlas: every cascade side by side (1×1 without shadows, never sampled then).
|
|
326
|
+
const sh = this.shadows;
|
|
327
|
+
this.#shadowAtlas = gl.createTexture();
|
|
328
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#shadowAtlas);
|
|
329
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.DEPTH_COMPONENT32F, sh ? sh.cascades * sh.size : 1, sh ? sh.size : 1);
|
|
330
|
+
this.#shadowFbo = gl.createFramebuffer();
|
|
331
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo);
|
|
332
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this.#shadowAtlas, 0);
|
|
333
|
+
gl.drawBuffers([gl.NONE]);
|
|
334
|
+
gl.readBuffer(gl.NONE);
|
|
335
|
+
// The environment cubes.
|
|
336
|
+
const cube = () => {
|
|
337
|
+
const t = gl.createTexture();
|
|
338
|
+
gl.bindTexture(gl.TEXTURE_CUBE_MAP, t);
|
|
339
|
+
gl.texStorage2D(gl.TEXTURE_CUBE_MAP, ENV_MIPS, this.hdr ? gl.RGBA16F : gl.RGBA8, ENV_SIZE, ENV_SIZE);
|
|
340
|
+
return t;
|
|
341
|
+
};
|
|
342
|
+
this.#envBase = cube();
|
|
343
|
+
this.#envCube = cube();
|
|
344
|
+
this.#envFbo = gl.createFramebuffer();
|
|
345
|
+
this.#envReadFbo = gl.createFramebuffer();
|
|
346
|
+
this.#sceneFbo = gl.createFramebuffer();
|
|
347
|
+
this.#ldrFbo = gl.createFramebuffer();
|
|
348
|
+
this.#palette = this.#paletteTexture(1);
|
|
349
|
+
const tex = (target, texture) => ({ target, texture });
|
|
350
|
+
// Bindings 4/6/9/10 (the texture arrays) are filled per frame: the store may replace them.
|
|
351
|
+
this.#drawBindings = {
|
|
352
|
+
uniforms: new Map([[0, this.#ubo.camera], [3, this.#ubo.materials], [18, this.#ubo.materialExt], [7, this.#ubo.lighting], [11, this.#ubo.lights], [12, this.#ubo.cluster], [14, this.#ubo.shadow]]),
|
|
353
|
+
textures: new Map([[8, tex(gl.TEXTURE_CUBE_MAP, this.#envCube)], [13, tex(gl.TEXTURE_2D, this.#binsTex)], [15, tex(gl.TEXTURE_2D, this.#shadowAtlas)], [17, tex(gl.TEXTURE_2D, this.#palette)], [19, tex(gl.TEXTURE_2D, this.#palette)]]), // 19: no opaque-scene copy on WebGL2 (ADR-0099)
|
|
354
|
+
samplers: new Map([[5, this.#samplers.material], [16, this.#samplers.compare]]),
|
|
355
|
+
};
|
|
356
|
+
this.buildEnvironment();
|
|
357
|
+
}
|
|
358
|
+
/** A palette texture `rows` rows tall (ADR-0088): read with texelFetch, so NEAREST and no mips. */
|
|
359
|
+
#paletteTexture(rows) {
|
|
360
|
+
const gl = this.gl;
|
|
361
|
+
const t = gl.createTexture();
|
|
362
|
+
gl.bindTexture(gl.TEXTURE_2D, t);
|
|
363
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, PALETTE_WIDTH, rows);
|
|
364
|
+
for (const [k, v] of [[gl.TEXTURE_MIN_FILTER, gl.NEAREST], [gl.TEXTURE_MAG_FILTER, gl.NEAREST]])
|
|
365
|
+
gl.texParameteri(gl.TEXTURE_2D, k, v);
|
|
366
|
+
return t;
|
|
367
|
+
}
|
|
368
|
+
/** Uploads this frame's palette (as `SceneRenderer`'s): full rows, then the partial last one. */
|
|
369
|
+
#syncPalette() {
|
|
370
|
+
const view = this.paletteSource?.();
|
|
371
|
+
if (!view || view.rows === 0)
|
|
372
|
+
return;
|
|
373
|
+
const gl = this.gl;
|
|
374
|
+
const texels = view.rows * 3;
|
|
375
|
+
const rows = Math.ceil(texels / PALETTE_WIDTH);
|
|
376
|
+
if (rows > this.#paletteRows) {
|
|
377
|
+
gl.deleteTexture(this.#palette);
|
|
378
|
+
this.#paletteRows = 2 ** Math.ceil(Math.log2(rows));
|
|
379
|
+
this.#palette = this.#paletteTexture(this.#paletteRows);
|
|
380
|
+
this.#drawBindings.textures.set(17, { target: gl.TEXTURE_2D, texture: this.#palette });
|
|
381
|
+
}
|
|
382
|
+
// WebGL takes no view of a SharedArrayBuffer (the threaded core's memory): copy it out.
|
|
383
|
+
const src = (view.f32.buffer instanceof ArrayBuffer ? view.f32.subarray(view.at, view.at + texels * 4) : view.f32.slice(view.at, view.at + texels * 4));
|
|
384
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#palette);
|
|
385
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
|
|
386
|
+
const full = Math.floor(texels / PALETTE_WIDTH);
|
|
387
|
+
if (full > 0)
|
|
388
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, PALETTE_WIDTH, full, gl.RGBA, gl.FLOAT, src, 0);
|
|
389
|
+
const rest = texels - full * PALETTE_WIDTH;
|
|
390
|
+
if (rest > 0)
|
|
391
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, full, rest, 1, gl.RGBA, gl.FLOAT, src, full * PALETTE_WIDTH * 4);
|
|
392
|
+
}
|
|
393
|
+
/** Always ready: WebGL2 draws node materials as the standard material (ADR-0070). */
|
|
394
|
+
async shadersReady() { }
|
|
395
|
+
/** GL programs compile when first drawn: nothing to wait for. */
|
|
396
|
+
async drawClassesReady() { }
|
|
397
|
+
/** The current sky. */
|
|
398
|
+
get sky() {
|
|
399
|
+
return this.#sky;
|
|
400
|
+
}
|
|
401
|
+
/** Changes the sky (unset fields keep their values) and rebuilds the environment. */
|
|
402
|
+
setSky(sky) {
|
|
403
|
+
this.#sky = mergeSky(this.#sky, sky);
|
|
404
|
+
this.buildEnvironment();
|
|
405
|
+
}
|
|
406
|
+
/** Renders the sky into every level of the base cube, copies level 0 and prefilters the rest; projects SH9. */
|
|
407
|
+
buildEnvironment() {
|
|
408
|
+
const gl = this.gl;
|
|
409
|
+
const slot = Math.ceil(SKY_PARAMS_BYTES / this.#uboAlign) * this.#uboAlign;
|
|
410
|
+
const data = new Float32Array((slot * 6 * ENV_MIPS) / 4);
|
|
411
|
+
for (let m = 0; m < ENV_MIPS; m++)
|
|
412
|
+
for (let f = 0; f < 6; f++)
|
|
413
|
+
packSkyParams(data, ((m * 6 + f) * slot) / 4, this.#sky, m, f);
|
|
414
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.sky);
|
|
415
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
|
|
416
|
+
gl.disable(gl.DEPTH_TEST);
|
|
417
|
+
gl.disable(gl.CULL_FACE);
|
|
418
|
+
gl.bindVertexArray(this.#emptyVao);
|
|
419
|
+
const face = (target, m, f, program, bindings) => {
|
|
420
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#envFbo);
|
|
421
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + f, target, m);
|
|
422
|
+
const s = Math.max(1, ENV_SIZE >> m);
|
|
423
|
+
gl.viewport(0, 0, s, s);
|
|
424
|
+
program.bind(gl, bindings);
|
|
425
|
+
gl.bindBufferRange(gl.UNIFORM_BUFFER, 0, this.#ubo.sky, (m * 6 + f) * slot, SKY_PARAMS_BYTES);
|
|
426
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
427
|
+
};
|
|
428
|
+
const skyBindings = { uniforms: new Map([[0, this.#ubo.sky]]), textures: new Map(), samplers: new Map() };
|
|
429
|
+
for (let m = 0; m < ENV_MIPS; m++)
|
|
430
|
+
for (let f = 0; f < 6; f++)
|
|
431
|
+
face(this.#envBase, m, f, this.#programs.sky, skyBindings);
|
|
432
|
+
// Level 0 as is (the copy WebGPU makes), by a blit: the same format, no filtering.
|
|
433
|
+
for (let f = 0; f < 6; f++) {
|
|
434
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.#envReadFbo);
|
|
435
|
+
gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + f, this.#envBase, 0);
|
|
436
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.#envFbo);
|
|
437
|
+
gl.framebufferTexture2D(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + f, this.#envCube, 0);
|
|
438
|
+
gl.blitFramebuffer(0, 0, ENV_SIZE, ENV_SIZE, 0, 0, ENV_SIZE, ENV_SIZE, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
439
|
+
}
|
|
440
|
+
const filterBindings = { uniforms: new Map([[0, this.#ubo.sky]]), textures: new Map([[1, { target: gl.TEXTURE_CUBE_MAP, texture: this.#envBase }]]), samplers: new Map([[2, this.#samplers.linear]]) };
|
|
441
|
+
for (let m = 1; m < ENV_MIPS; m++)
|
|
442
|
+
for (let f = 0; f < 6; f++)
|
|
443
|
+
face(this.#envCube, m, f, this.#programs.prefilter, filterBindings);
|
|
444
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
445
|
+
skySh9(this.#sh9, this.#sky);
|
|
446
|
+
this.#envVersion++;
|
|
447
|
+
}
|
|
448
|
+
/** The CPU cull's parameters (as `SceneRenderer.writeCullParams`: with shadows, the planes swept towards the light). */
|
|
449
|
+
/** The shading level in use, clamped to those compiled. */
|
|
450
|
+
get #level() {
|
|
451
|
+
return Math.max(0, Math.min(this.#levelPrograms.length - 1, Math.round(this.shadingLevel)));
|
|
452
|
+
}
|
|
453
|
+
writeCullParams(f32, at, camera, heightPx, widthPx = heightPx) {
|
|
454
|
+
// Level 2 casts no shadows: the plain frustum, no off-screen casters.
|
|
455
|
+
const casting = !!this.shadows && this.#level < 2;
|
|
456
|
+
if (casting)
|
|
457
|
+
sweptPlanes(camera, f32, at);
|
|
458
|
+
else
|
|
459
|
+
for (let k = 0; k < 24; k++)
|
|
460
|
+
f32[at + k] = camera.uniform[16 + k];
|
|
461
|
+
// Casters per cascade (ADR-0073): fit now (the upload's fit is the same), then the boxes.
|
|
462
|
+
const sh = this.shadows;
|
|
463
|
+
const partition = casting && sh && sh.cascades <= CULL_CASCADES ? sh.cascades : 0;
|
|
464
|
+
f32[at + 27] = partition;
|
|
465
|
+
if (partition > 0) {
|
|
466
|
+
fitCascades(sh, camera, widthPx / Math.max(1, heightPx), this.#shadowParams);
|
|
467
|
+
cascadeCullPlanes(this.#shadowParams, partition, f32, at + 28);
|
|
468
|
+
}
|
|
469
|
+
const scale = 2 ** this.lodBias;
|
|
470
|
+
f32[at + 24] = heightPx / (2 * Math.tan(camera.fovY / 2));
|
|
471
|
+
f32[at + 25] = this.errorPx * scale;
|
|
472
|
+
f32[at + 26] = Math.max(this.minRadiusPx, this.lodBias > 0 ? 0.25 * scale : 0);
|
|
473
|
+
}
|
|
474
|
+
/** Instances drawn at each LOD (index 0 = full detail). A diagnostic: it allocates. */
|
|
475
|
+
drawnByLod() {
|
|
476
|
+
let last = 0;
|
|
477
|
+
for (let l = 0; l < 8; l++)
|
|
478
|
+
if (this.#byLod[l])
|
|
479
|
+
last = l + 1;
|
|
480
|
+
return Array.from(this.#byLod.subarray(0, Math.max(1, last)));
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Uploads this frame: the camera, the lights (packed and binned), the shadow fit, the
|
|
484
|
+
* materials and geometry that changed, and the CPU-culled instances from linear memory.
|
|
485
|
+
*/
|
|
486
|
+
upload(heap, u32, x, camera, width, height, world, Light) {
|
|
487
|
+
const gl = this.gl;
|
|
488
|
+
this.#syncGeometry();
|
|
489
|
+
this.#syncPalette();
|
|
490
|
+
this.#syncMaterials();
|
|
491
|
+
this.#syncLighting();
|
|
492
|
+
this.#cameraData.set(camera.uniform);
|
|
493
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.camera);
|
|
494
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.#cameraData);
|
|
495
|
+
// Lights: packed, binned on the CPU (the bins texture only as wide as the fullest row).
|
|
496
|
+
const k = this.lights;
|
|
497
|
+
k.pack(world, Light, camera, width, height);
|
|
498
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.lights);
|
|
499
|
+
if (k.count > 0)
|
|
500
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, k.lights, 0, k.count * 16);
|
|
501
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.cluster);
|
|
502
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, k.params);
|
|
503
|
+
if (k.count > 0 || this.#binnedLights > 0) {
|
|
504
|
+
binLights(k.lights, k.params, this.#bins, this.#binCounts);
|
|
505
|
+
let widest = 0;
|
|
506
|
+
for (let i = 0; i < CLUSTERS; i++)
|
|
507
|
+
widest = Math.max(widest, this.#binCounts[i]);
|
|
508
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#binsTex);
|
|
509
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, CLUSTER_ROW);
|
|
510
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, widest + 1, CLUSTERS, gl.RED_INTEGER, gl.UNSIGNED_INT, this.#bins);
|
|
511
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
512
|
+
this.#binnedLights = k.count;
|
|
513
|
+
}
|
|
514
|
+
// Shadows: the same fit as WebGPU's.
|
|
515
|
+
const sh = this.shadows;
|
|
516
|
+
if (sh) {
|
|
517
|
+
fitCascades(sh, camera, width / Math.max(1, height), this.#shadowParams);
|
|
518
|
+
this.#shadowParams[77] = this.#manualPcf ? 1 : 0; // `shadow.pcf.y`: filter the comparisons in the shader
|
|
519
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.shadow);
|
|
520
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.#shadowParams);
|
|
521
|
+
const slot = Math.ceil(CASCADE_UNIFORM_BYTES / this.#uboAlign) * this.#uboAlign;
|
|
522
|
+
for (let c = 0; c < sh.cascades; c++)
|
|
523
|
+
this.#cascadeData.set(this.#shadowParams.subarray(c * 16, c * 16 + 16), (c * slot) / 4);
|
|
524
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.cascades);
|
|
525
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.#cascadeData);
|
|
526
|
+
}
|
|
527
|
+
if (x)
|
|
528
|
+
this.#uploadCulled(heap, u32, x);
|
|
529
|
+
else {
|
|
530
|
+
this.#drawCount = 0;
|
|
531
|
+
this.count = 0;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
#uploadCulled(heap, u32, x) {
|
|
535
|
+
const gl = this.gl;
|
|
536
|
+
const bytes = x.count * CULLED_BYTES;
|
|
537
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.#instanceBuf);
|
|
538
|
+
if (this.#instanceBytes < bytes) {
|
|
539
|
+
this.#instanceBytes = Math.max(1 << 16, 2 ** Math.ceil(Math.log2(bytes)));
|
|
540
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.#instanceBytes, gl.STREAM_DRAW);
|
|
541
|
+
}
|
|
542
|
+
// WebGL takes no view of a SharedArrayBuffer (the threaded core's memory): copy it out first.
|
|
543
|
+
if (bytes > 0)
|
|
544
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, heap.buffer instanceof ArrayBuffer ? heap : heap.slice(x.address, x.address + bytes), heap.buffer instanceof ArrayBuffer ? x.address : 0, bytes);
|
|
545
|
+
this.transformBytes = bytes;
|
|
546
|
+
const draws = this.#culled;
|
|
547
|
+
draws.build(u32, x.countsAt >>> 2, x.meshes, this.#store.entries, x.cascades);
|
|
548
|
+
this.#draws = draws.main;
|
|
549
|
+
this.#byLod.set(draws.byLod);
|
|
550
|
+
this.#cascadesCulled = x.cascades;
|
|
551
|
+
this.#drawCount = draws.mainCount;
|
|
552
|
+
this.count = x.count;
|
|
553
|
+
this.visible = x.count;
|
|
554
|
+
}
|
|
555
|
+
/** The last `MeshStore.updateVertices` serial uploaded. */
|
|
556
|
+
#writesSeen = 0;
|
|
557
|
+
#syncGeometry() {
|
|
558
|
+
const s = this.#store;
|
|
559
|
+
if (this.#geometryVersion === s.version) {
|
|
560
|
+
this.#syncWrites();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const gl = this.gl;
|
|
564
|
+
const grow = (target, buf, need, have) => {
|
|
565
|
+
if (have >= need)
|
|
566
|
+
return have;
|
|
567
|
+
const size = Math.max(1 << 16, 2 ** Math.ceil(Math.log2(need)));
|
|
568
|
+
gl.bindBuffer(target, buf);
|
|
569
|
+
gl.bufferData(target, size, gl.STATIC_DRAW);
|
|
570
|
+
return size;
|
|
571
|
+
};
|
|
572
|
+
const vBytes = s.vertexCount * VERTEX_BYTES;
|
|
573
|
+
const iBytes = s.indexCount * 4;
|
|
574
|
+
if (vBytes > this.#vertexBytes) {
|
|
575
|
+
this.#vertexBytes = grow(gl.ARRAY_BUFFER, this.#vertexBuf, vBytes, this.#vertexBytes);
|
|
576
|
+
this.#vertexBlocks = 0;
|
|
577
|
+
}
|
|
578
|
+
if (iBytes > this.#indexBytes) {
|
|
579
|
+
// The index buffer is bound in the VAOs: bind one to resize it.
|
|
580
|
+
gl.bindVertexArray(this.#drawVao);
|
|
581
|
+
this.#indexBytes = grow(gl.ELEMENT_ARRAY_BUFFER, this.#indexBuf, iBytes, this.#indexBytes);
|
|
582
|
+
this.#indexBlocks = 0;
|
|
583
|
+
}
|
|
584
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.#vertexBuf);
|
|
585
|
+
for (; this.#vertexBlocks < s.vertexBlocks.length; this.#vertexBlocks++) {
|
|
586
|
+
const b = s.vertexBlocks[this.#vertexBlocks];
|
|
587
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, b.at * VERTEX_BYTES, b.data);
|
|
588
|
+
}
|
|
589
|
+
gl.bindVertexArray(this.#drawVao);
|
|
590
|
+
for (; this.#indexBlocks < s.indexBlocks.length; this.#indexBlocks++) {
|
|
591
|
+
const b = s.indexBlocks[this.#indexBlocks];
|
|
592
|
+
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, b.at * 4, b.data);
|
|
593
|
+
}
|
|
594
|
+
gl.bindVertexArray(null);
|
|
595
|
+
if (s.skinnedVertexCount > 0)
|
|
596
|
+
this.#syncSkinned();
|
|
597
|
+
this.#geometryVersion = s.version;
|
|
598
|
+
this.#syncWrites();
|
|
599
|
+
}
|
|
600
|
+
/** Rewritten vertices (`MeshStore.updateVertices`, morphs) and indices (`updateIndices`, ADR-0117) newer than the last upload. */
|
|
601
|
+
#syncWrites() {
|
|
602
|
+
const s = this.#store;
|
|
603
|
+
if (s.writeSerial === this.#writesSeen)
|
|
604
|
+
return;
|
|
605
|
+
const gl = this.gl;
|
|
606
|
+
for (const w of s.vertexWrites.values()) {
|
|
607
|
+
if (w.serial <= this.#writesSeen)
|
|
608
|
+
continue;
|
|
609
|
+
const buf = w.skinned ? this.#skinned?.vertexBuf : this.#vertexBuf;
|
|
610
|
+
if (!buf)
|
|
611
|
+
continue;
|
|
612
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
613
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, w.at * VERTEX_BYTES, w.data);
|
|
614
|
+
}
|
|
615
|
+
// The index buffer is bound in the draw VAO.
|
|
616
|
+
let bound = false;
|
|
617
|
+
for (const w of s.indexWrites.values()) {
|
|
618
|
+
if (w.serial <= this.#writesSeen)
|
|
619
|
+
continue;
|
|
620
|
+
if (!bound)
|
|
621
|
+
gl.bindVertexArray(this.#drawVao);
|
|
622
|
+
bound = true;
|
|
623
|
+
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, w.at * 4, w.data);
|
|
624
|
+
}
|
|
625
|
+
if (bound)
|
|
626
|
+
gl.bindVertexArray(null);
|
|
627
|
+
this.#writesSeen = s.writeSerial;
|
|
628
|
+
}
|
|
629
|
+
/** The skinned pool's vertices and skin stream (ADR-0088), in their own buffers and VAO. */
|
|
630
|
+
#syncSkinned() {
|
|
631
|
+
const gl = this.gl;
|
|
632
|
+
const s = this.#store;
|
|
633
|
+
if (!this.#skinned) {
|
|
634
|
+
const vao = gl.createVertexArray();
|
|
635
|
+
gl.bindVertexArray(vao);
|
|
636
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#indexBuf);
|
|
637
|
+
for (let loc = 0; loc < 8; loc++)
|
|
638
|
+
gl.enableVertexAttribArray(loc);
|
|
639
|
+
for (const loc of [2, 3, 4])
|
|
640
|
+
gl.vertexAttribDivisor(loc, 1);
|
|
641
|
+
gl.bindVertexArray(null);
|
|
642
|
+
this.#skinned = { vertexBuf: gl.createBuffer(), skinBuf: gl.createBuffer(), vertexBytes: 0, skinBytes: 0, blocks: 0, vao };
|
|
643
|
+
}
|
|
644
|
+
const k = this.#skinned;
|
|
645
|
+
if (s.skinnedVertexCount * VERTEX_BYTES > k.vertexBytes) {
|
|
646
|
+
// Both buffers regrow together and are rewritten whole: the blocks restart.
|
|
647
|
+
const n = Math.max(1 << 10, 2 ** Math.ceil(Math.log2(s.skinnedVertexCount)));
|
|
648
|
+
k.vertexBytes = n * VERTEX_BYTES;
|
|
649
|
+
k.skinBytes = n * SKIN_STREAM_BYTES;
|
|
650
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, k.vertexBuf);
|
|
651
|
+
gl.bufferData(gl.ARRAY_BUFFER, k.vertexBytes, gl.STATIC_DRAW);
|
|
652
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, k.skinBuf);
|
|
653
|
+
gl.bufferData(gl.ARRAY_BUFFER, k.skinBytes, gl.STATIC_DRAW);
|
|
654
|
+
k.blocks = 0;
|
|
655
|
+
}
|
|
656
|
+
for (; k.blocks < s.skinnedVertexBlocks.length; k.blocks++) {
|
|
657
|
+
const v = s.skinnedVertexBlocks[k.blocks];
|
|
658
|
+
const w = s.skinBlocks[k.blocks];
|
|
659
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, k.vertexBuf);
|
|
660
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, v.at * VERTEX_BYTES, v.data);
|
|
661
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, k.skinBuf);
|
|
662
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, w.at * SKIN_STREAM_BYTES, w.data);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
#syncMaterials() {
|
|
666
|
+
const m = this.#materials;
|
|
667
|
+
m.sync(this.#textures.version);
|
|
668
|
+
if (this.#materialVersion === m.version)
|
|
669
|
+
return;
|
|
670
|
+
const gl = this.gl;
|
|
671
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.materials);
|
|
672
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, m.data);
|
|
673
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.materialExt);
|
|
674
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, m.ext);
|
|
675
|
+
this.#materialVersion = m.version;
|
|
676
|
+
}
|
|
677
|
+
#syncLighting() {
|
|
678
|
+
const l = this.lighting;
|
|
679
|
+
const c = l.sunColor;
|
|
680
|
+
const inline = this.inlineTone;
|
|
681
|
+
const curve = inline ? CURVES[this.curve] : -1;
|
|
682
|
+
const exposure = inline ? this.exposure : 0;
|
|
683
|
+
if (c[0] === this.#lit[0] && c[1] === this.#lit[1] && c[2] === this.#lit[2] && l.sunIntensity === this.#lit[3] && l.ambient === this.#lit[4] && exposure === this.#lit[5] && curve === this.#lit[6] && this.#envVersion === this.#litEnvVersion)
|
|
684
|
+
return;
|
|
685
|
+
this.#lit.set([c[0], c[1], c[2], l.sunIntensity, l.ambient, exposure, curve]);
|
|
686
|
+
this.#clear = inline ? [...toneMapColor(CLEAR, this.exposure, this.curve), 1] : [...CLEAR];
|
|
687
|
+
this.#litEnvVersion = this.#envVersion;
|
|
688
|
+
const d = this.#lightingData;
|
|
689
|
+
d[0] = c[0] * l.sunIntensity;
|
|
690
|
+
d[1] = c[1] * l.sunIntensity;
|
|
691
|
+
d[2] = c[2] * l.sunIntensity;
|
|
692
|
+
d[3] = l.ambient;
|
|
693
|
+
d[4] = ENV_MIPS;
|
|
694
|
+
d[5] = inline ? 1 : 0;
|
|
695
|
+
d[6] = exposure;
|
|
696
|
+
d[7] = Math.max(0, curve);
|
|
697
|
+
d.set(this.#sh9, 8);
|
|
698
|
+
const gl = this.gl;
|
|
699
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.lighting);
|
|
700
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, d);
|
|
701
|
+
}
|
|
702
|
+
#resize(width, height) {
|
|
703
|
+
if (width === this.#width && height === this.#height)
|
|
704
|
+
return;
|
|
705
|
+
const gl = this.gl;
|
|
706
|
+
this.#width = width;
|
|
707
|
+
this.#height = height;
|
|
708
|
+
if (this.#hdrTex)
|
|
709
|
+
gl.deleteTexture(this.#hdrTex);
|
|
710
|
+
if (this.#depthRb)
|
|
711
|
+
gl.deleteRenderbuffer(this.#depthRb);
|
|
712
|
+
if (this.#ldrRb)
|
|
713
|
+
gl.deleteRenderbuffer(this.#ldrRb);
|
|
714
|
+
this.#hdrTex = null;
|
|
715
|
+
this.#depthRb = gl.createRenderbuffer();
|
|
716
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, this.#depthRb);
|
|
717
|
+
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT32F, width, height);
|
|
718
|
+
this.#ldrRb = gl.createRenderbuffer();
|
|
719
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, this.#ldrRb);
|
|
720
|
+
gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, width, height);
|
|
721
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#ldrFbo);
|
|
722
|
+
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, this.#ldrRb);
|
|
723
|
+
if (this.inlineTone) {
|
|
724
|
+
// The scene draws straight into the RGBA8 target.
|
|
725
|
+
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.#depthRb);
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
this.#hdrTex = gl.createTexture();
|
|
729
|
+
gl.bindTexture(gl.TEXTURE_2D, this.#hdrTex);
|
|
730
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, this.hdr ? gl.RGBA16F : gl.RGBA8, width, height);
|
|
731
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#sceneFbo);
|
|
732
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.#hdrTex, 0);
|
|
733
|
+
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.#depthRb);
|
|
734
|
+
}
|
|
735
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Re-points the vertex attributes at draw `i`'s mesh (base vertex) and instances (first
|
|
739
|
+
* instance). A skinned draw (the skinned VAO bound) reads the skinned pool and its skin
|
|
740
|
+
* stream, and its shadow reads the ids too (the skin row is in the material word).
|
|
741
|
+
*/
|
|
742
|
+
#point(d, i, shadow, skinned) {
|
|
743
|
+
const gl = this.gl;
|
|
744
|
+
const v = d[i * 5 + 2];
|
|
745
|
+
const base = v * VERTEX_BYTES;
|
|
746
|
+
const first = d[i * 5 + 3] * CULLED_BYTES;
|
|
747
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, skinned ? this.#skinned.vertexBuf : this.#vertexBuf);
|
|
748
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, VERTEX_BYTES, base);
|
|
749
|
+
if (!shadow || skinned) {
|
|
750
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, VERTEX_BYTES, base + 12);
|
|
751
|
+
gl.vertexAttribPointer(5, 2, gl.FLOAT, false, VERTEX_BYTES, base + 24);
|
|
752
|
+
}
|
|
753
|
+
if (skinned) {
|
|
754
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.#skinned.skinBuf);
|
|
755
|
+
gl.vertexAttribIPointer(6, 4, gl.UNSIGNED_SHORT, SKIN_STREAM_BYTES, v * SKIN_STREAM_BYTES);
|
|
756
|
+
gl.vertexAttribPointer(7, 4, gl.UNSIGNED_SHORT, true, SKIN_STREAM_BYTES, v * SKIN_STREAM_BYTES + 8);
|
|
757
|
+
}
|
|
758
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.#instanceBuf);
|
|
759
|
+
gl.vertexAttribPointer(2, 4, gl.FLOAT, false, CULLED_BYTES, first);
|
|
760
|
+
gl.vertexAttribPointer(3, 4, gl.FLOAT, false, CULLED_BYTES, first + 16);
|
|
761
|
+
if (!shadow || skinned)
|
|
762
|
+
gl.vertexAttribIPointer(4, 2, gl.UNSIGNED_INT, CULLED_BYTES, first + 32);
|
|
763
|
+
}
|
|
764
|
+
#drawAll(shadow, d = this.#draws, count = this.#drawCount, classes = this.#culled.mainClass, want = () => true) {
|
|
765
|
+
const gl = this.gl;
|
|
766
|
+
for (let i = 0; i < count; i++) {
|
|
767
|
+
if (!want(classes[i]))
|
|
768
|
+
continue;
|
|
769
|
+
this.#point(d, i, shadow, isSkinnedClass(classes[i]));
|
|
770
|
+
gl.drawElementsInstanced(gl.TRIANGLES, d[i * 5], gl.UNSIGNED_INT, d[i * 5 + 1] * 4, d[i * 5 + 4]);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Draws the frame (shadows, scene, tone curve) and presents it to the canvas, `width × height`. */
|
|
774
|
+
render(width, height) {
|
|
775
|
+
const gl = this.gl;
|
|
776
|
+
this.#resize(width, height);
|
|
777
|
+
const t = this.#textures;
|
|
778
|
+
const tb = this.#drawBindings.textures;
|
|
779
|
+
tb.set(4, { target: gl.TEXTURE_2D_ARRAY, texture: t.array('srgb', false) });
|
|
780
|
+
tb.set(6, { target: gl.TEXTURE_2D_ARRAY, texture: t.array('linear', false) });
|
|
781
|
+
tb.set(9, { target: gl.TEXTURE_2D_ARRAY, texture: t.array('srgb', true) });
|
|
782
|
+
tb.set(10, { target: gl.TEXTURE_2D_ARRAY, texture: t.array('linear', true) });
|
|
783
|
+
gl.enable(gl.DEPTH_TEST);
|
|
784
|
+
gl.depthFunc(gl.GREATER);
|
|
785
|
+
gl.depthMask(true);
|
|
786
|
+
gl.enable(gl.CULL_FACE);
|
|
787
|
+
gl.cullFace(gl.BACK);
|
|
788
|
+
// The clip-space flip reverses winding: WebGPU's counter-clockwise front faces are clockwise here.
|
|
789
|
+
gl.frontFace(gl.CW);
|
|
790
|
+
gl.clearDepth(0);
|
|
791
|
+
// 1. The shadow cascades (none at shading level 2).
|
|
792
|
+
const level = this.#level;
|
|
793
|
+
const sh = level < 2 ? this.shadows : null;
|
|
794
|
+
if (sh) {
|
|
795
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#shadowFbo);
|
|
796
|
+
gl.viewport(0, 0, sh.cascades * sh.size, sh.size);
|
|
797
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
798
|
+
if (this.#drawCount > 0) {
|
|
799
|
+
gl.bindVertexArray(this.#shadowVao);
|
|
800
|
+
this.#programs.shadow.bind(gl, { uniforms: new Map([[0, this.#ubo.cascades]]), textures: new Map(), samplers: new Map() });
|
|
801
|
+
const slot = Math.ceil(CASCADE_UNIFORM_BYTES / this.#uboAlign) * this.#uboAlign;
|
|
802
|
+
for (let c = 0; c < sh.cascades; c++) {
|
|
803
|
+
gl.viewport(c * sh.size, 0, sh.size, sh.size);
|
|
804
|
+
gl.bindBufferRange(gl.UNIFORM_BUFFER, 0, this.#ubo.cascades, c * slot, CASCADE_UNIFORM_BYTES);
|
|
805
|
+
// Only this cascade's casters when the cull partitioned them (ADR-0073); blended meshes
|
|
806
|
+
// cast nothing, double-sided ones draw both faces (ADR-0081).
|
|
807
|
+
const part = c < this.#cascadesCulled;
|
|
808
|
+
const d = part ? this.#culled.cascade[c] : this.#draws;
|
|
809
|
+
const n = part ? this.#culled.cascadeCount[c] : this.#drawCount;
|
|
810
|
+
const cls = part ? this.#culled.cascadeClass[c] : this.#culled.mainClass;
|
|
811
|
+
gl.enable(gl.CULL_FACE);
|
|
812
|
+
this.#drawAll(true, d, n, cls, (k) => !isBlendClass(k) && !isSkinnedClass(k) && (k & 1) === 0);
|
|
813
|
+
gl.disable(gl.CULL_FACE);
|
|
814
|
+
this.#drawAll(true, d, n, cls, (k) => !isBlendClass(k) && !isSkinnedClass(k) && (k & 1) === 1);
|
|
815
|
+
}
|
|
816
|
+
// Skinned casters (ADR-0088): their own program and VAO, every cascade again.
|
|
817
|
+
if (this.#skinned && this.#hasSkinned()) {
|
|
818
|
+
gl.bindVertexArray(this.#skinned.vao);
|
|
819
|
+
this.#skinnedShadow ??= new GlProgram(gl, 'shadow:skinned', { vs: glVsShadowSkinned, fs: glShadow.fs });
|
|
820
|
+
this.#skinnedShadow.bind(gl, { uniforms: new Map([[0, this.#ubo.cascades]]), textures: new Map([[3, { target: gl.TEXTURE_2D, texture: this.#palette }]]), samplers: new Map() });
|
|
821
|
+
for (let c = 0; c < sh.cascades; c++) {
|
|
822
|
+
gl.viewport(c * sh.size, 0, sh.size, sh.size);
|
|
823
|
+
gl.bindBufferRange(gl.UNIFORM_BUFFER, 0, this.#ubo.cascades, c * slot, CASCADE_UNIFORM_BYTES);
|
|
824
|
+
const part = c < this.#cascadesCulled;
|
|
825
|
+
const d = part ? this.#culled.cascade[c] : this.#draws;
|
|
826
|
+
const n = part ? this.#culled.cascadeCount[c] : this.#drawCount;
|
|
827
|
+
const cls = part ? this.#culled.cascadeClass[c] : this.#culled.mainClass;
|
|
828
|
+
gl.enable(gl.CULL_FACE);
|
|
829
|
+
this.#drawAll(true, d, n, cls, (k) => !isBlendClass(k) && isSkinnedClass(k) && (k & 1) === 0);
|
|
830
|
+
gl.disable(gl.CULL_FACE);
|
|
831
|
+
this.#drawAll(true, d, n, cls, (k) => !isBlendClass(k) && isSkinnedClass(k) && (k & 1) === 1);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
gl.enable(gl.CULL_FACE);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// 2. The scene (straight into RGBA8 when the draw tone-maps inline).
|
|
838
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.inlineTone ? this.#ldrFbo : this.#sceneFbo);
|
|
839
|
+
gl.viewport(0, 0, width, height);
|
|
840
|
+
const k = this.#clear;
|
|
841
|
+
gl.clearColor(k[0], k[1], k[2], 1);
|
|
842
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
843
|
+
if (this.#drawCount > 0) {
|
|
844
|
+
// Class by class (ADR-0081): opaque, mask, then blend over them without depth writes;
|
|
845
|
+
// the skinned classes (ADR-0088) after each rigid group, with their own VAO.
|
|
846
|
+
const cls = this.#culled.mainClass;
|
|
847
|
+
for (const c of CLASS_ORDER) {
|
|
848
|
+
let any = false;
|
|
849
|
+
for (let i = 0; i < this.#drawCount && !any; i++)
|
|
850
|
+
any = cls[i] === c;
|
|
851
|
+
if (!any)
|
|
852
|
+
continue;
|
|
853
|
+
gl.bindVertexArray(isSkinnedClass(c) ? this.#skinned.vao : this.#drawVao);
|
|
854
|
+
if (c & 1)
|
|
855
|
+
gl.disable(gl.CULL_FACE);
|
|
856
|
+
else
|
|
857
|
+
gl.enable(gl.CULL_FACE);
|
|
858
|
+
if (isBlendClass(c)) {
|
|
859
|
+
gl.enable(gl.BLEND);
|
|
860
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
861
|
+
gl.depthMask(false);
|
|
862
|
+
}
|
|
863
|
+
this.#programFor(c, level).bind(gl, this.#drawBindings);
|
|
864
|
+
this.#drawAll(false, this.#draws, this.#drawCount, cls, (k) => k === c);
|
|
865
|
+
}
|
|
866
|
+
gl.disable(gl.BLEND);
|
|
867
|
+
gl.depthMask(true);
|
|
868
|
+
gl.enable(gl.CULL_FACE);
|
|
869
|
+
}
|
|
870
|
+
this.stats.draws = this.#drawCount;
|
|
871
|
+
// Debug lines (ADR-0087): over the scene, depth-tested, without writing depth.
|
|
872
|
+
if (this.debug && this.debug.count > 0)
|
|
873
|
+
this.#drawDebug(this.debug);
|
|
874
|
+
// 3. The tone curve, into RGBA8, then to the canvas (flipped back).
|
|
875
|
+
gl.disable(gl.DEPTH_TEST);
|
|
876
|
+
gl.disable(gl.CULL_FACE);
|
|
877
|
+
writeToneParams(new DataView(this.#toneData), this.exposure, this.curve, 0, this.grading);
|
|
878
|
+
gl.bindBuffer(gl.UNIFORM_BUFFER, this.#ubo.tone);
|
|
879
|
+
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.#toneData);
|
|
880
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.#ldrFbo);
|
|
881
|
+
if (!this.inlineTone) {
|
|
882
|
+
gl.viewport(0, 0, width, height);
|
|
883
|
+
gl.bindVertexArray(this.#emptyVao);
|
|
884
|
+
this.#programs.tonemap.bind(gl, { uniforms: new Map([[1, this.#ubo.tone]]), textures: new Map([[0, { target: gl.TEXTURE_2D, texture: this.#hdrTex }], [2, { target: gl.TEXTURE_2D, texture: this.#hdrTex }]]), samplers: new Map() });
|
|
885
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
886
|
+
}
|
|
887
|
+
if (this.#captures.length) {
|
|
888
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
889
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
|
|
890
|
+
for (const resolve of this.#captures.splice(0))
|
|
891
|
+
resolve({ width, height, rgba });
|
|
892
|
+
}
|
|
893
|
+
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.#ldrFbo);
|
|
894
|
+
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
|
895
|
+
gl.blitFramebuffer(0, 0, width, height, 0, height, width, 0, gl.COLOR_BUFFER_BIT, gl.NEAREST);
|
|
896
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
897
|
+
gl.bindVertexArray(null);
|
|
898
|
+
}
|
|
899
|
+
/** Whether this frame draws any skinned class. */
|
|
900
|
+
#hasSkinned() {
|
|
901
|
+
const cls = this.#culled.mainClass;
|
|
902
|
+
for (let i = 0; i < this.#drawCount; i++)
|
|
903
|
+
if (isSkinnedClass(cls[i]))
|
|
904
|
+
return true;
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
/** The next frame as drawn (rows top first: the offscreen targets have WebGPU's row order). */
|
|
908
|
+
capture() {
|
|
909
|
+
return new Promise((resolve) => this.#captures.push(resolve));
|
|
910
|
+
}
|
|
911
|
+
/** Bytes of the geometry and instance buffers (the memory counter). */
|
|
912
|
+
get gpuBytes() {
|
|
913
|
+
return this.#vertexBytes + this.#indexBytes + this.#instanceBytes + (this.#skinned ? this.#skinned.vertexBytes + this.#skinned.skinBytes : 0) + this.#paletteRows * PALETTE_WIDTH * 16;
|
|
914
|
+
}
|
|
915
|
+
/** GL buffers this renderer owns (the dispose leak check). */
|
|
916
|
+
get liveBuffers() {
|
|
917
|
+
return 3 + Object.keys(this.#ubo).length + (this.#lines ? 1 : 0) + (this.#skinned ? 2 : 0);
|
|
918
|
+
}
|
|
919
|
+
/** Debug line segments drawn over the scene each frame (the render plugin's `render.debug`). */
|
|
920
|
+
debug = null;
|
|
921
|
+
#lines = null;
|
|
922
|
+
#drawDebug(lines) {
|
|
923
|
+
const gl = this.gl;
|
|
924
|
+
if (!this.#lines) {
|
|
925
|
+
const program = new GlProgram(gl, 'lines', glLines);
|
|
926
|
+
const vao = gl.createVertexArray();
|
|
927
|
+
const buffer = gl.createBuffer();
|
|
928
|
+
gl.bindVertexArray(vao);
|
|
929
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
930
|
+
gl.enableVertexAttribArray(0);
|
|
931
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, DEBUG_VERTEX_BYTES, 0);
|
|
932
|
+
gl.enableVertexAttribArray(1);
|
|
933
|
+
gl.vertexAttribPointer(1, 4, gl.UNSIGNED_BYTE, true, DEBUG_VERTEX_BYTES, 12);
|
|
934
|
+
this.#lines = { program, vao, buffer };
|
|
935
|
+
}
|
|
936
|
+
const l = this.#lines;
|
|
937
|
+
gl.bindVertexArray(l.vao);
|
|
938
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, l.buffer);
|
|
939
|
+
gl.bufferData(gl.ARRAY_BUFFER, lines.data, gl.STREAM_DRAW, 0, lines.count * 4);
|
|
940
|
+
l.program.bind(gl, { uniforms: new Map([[0, this.#ubo.camera]]), textures: new Map(), samplers: new Map() });
|
|
941
|
+
gl.depthMask(false);
|
|
942
|
+
gl.drawArrays(gl.LINES, 0, lines.count);
|
|
943
|
+
gl.depthMask(true);
|
|
944
|
+
}
|
|
945
|
+
destroy() {
|
|
946
|
+
const gl = this.gl;
|
|
947
|
+
for (const b of [this.#vertexBuf, this.#indexBuf, this.#instanceBuf, ...Object.values(this.#ubo)])
|
|
948
|
+
gl.deleteBuffer(b);
|
|
949
|
+
for (const t of [this.#binsTex, this.#shadowAtlas, this.#envBase, this.#envCube, this.#hdrTex, this.#palette])
|
|
950
|
+
if (t)
|
|
951
|
+
gl.deleteTexture(t);
|
|
952
|
+
if (this.#skinned) {
|
|
953
|
+
gl.deleteBuffer(this.#skinned.vertexBuf);
|
|
954
|
+
gl.deleteBuffer(this.#skinned.skinBuf);
|
|
955
|
+
gl.deleteVertexArray(this.#skinned.vao);
|
|
956
|
+
this.#skinned = null;
|
|
957
|
+
}
|
|
958
|
+
for (const r of [this.#depthRb, this.#ldrRb])
|
|
959
|
+
if (r)
|
|
960
|
+
gl.deleteRenderbuffer(r);
|
|
961
|
+
for (const f of [this.#sceneFbo, this.#ldrFbo, this.#shadowFbo, this.#envFbo, this.#envReadFbo])
|
|
962
|
+
gl.deleteFramebuffer(f);
|
|
963
|
+
for (const v of [this.#drawVao, this.#shadowVao, this.#emptyVao])
|
|
964
|
+
gl.deleteVertexArray(v);
|
|
965
|
+
if (this.#lines) {
|
|
966
|
+
gl.deleteBuffer(this.#lines.buffer);
|
|
967
|
+
gl.deleteVertexArray(this.#lines.vao);
|
|
968
|
+
this.#lines = null;
|
|
969
|
+
}
|
|
970
|
+
for (const p of new Set([...Object.values(this.#programs), ...this.#levelPrograms, ...this.#alphaPrograms.flat().filter((x) => !!x)]))
|
|
971
|
+
gl.deleteProgram(p.program);
|
|
972
|
+
for (const s of Object.values(this.#samplers))
|
|
973
|
+
gl.deleteSampler(s);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
//# sourceMappingURL=renderer.js.map
|