pelican-engine 0.0.0-stage → 0.3.1
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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import * as THREE from "three/webgpu";
|
|
2
|
+
import * as TSL from "three/tsl";
|
|
3
|
+
import { typedGraphToTSL } from "./graph-tsl.js";
|
|
4
|
+
const fail = (code, message) => {
|
|
5
|
+
throw Error("TC_POSTPROCESS_" + code + ": " + message);
|
|
6
|
+
};
|
|
7
|
+
// Three's public getter copies a Vector4 with additional depth-range properties.
|
|
8
|
+
class ViewportSnapshot extends THREE.Vector4 {
|
|
9
|
+
minDepth = 0;
|
|
10
|
+
maxDepth = 1;
|
|
11
|
+
copy(v) {
|
|
12
|
+
super.copy(v);
|
|
13
|
+
const range = v;
|
|
14
|
+
this.minDepth = range.minDepth ?? 0;
|
|
15
|
+
this.maxDepth = range.maxDepth ?? 1;
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Private presentation owner. Call only after renderer.init(); never expose to games. */
|
|
20
|
+
export function createPostProcessComposer(renderer, width, height, definitions, textures, buffers, managedTextures) {
|
|
21
|
+
const backend = renderer.backend;
|
|
22
|
+
const device = backend.device;
|
|
23
|
+
// currentSamples is zero at the final output-conversion stage, not the scene's MSAA count.
|
|
24
|
+
const samples = renderer.samples;
|
|
25
|
+
if (![width, height].every((n) => Number.isSafeInteger(n) && n > 0))
|
|
26
|
+
fail("CAPABILITY", "Positive integer target dimensions are required.");
|
|
27
|
+
if (!Number.isSafeInteger(samples) || samples < 0)
|
|
28
|
+
fail("CAPABILITY", "Cannot determine the renderer's actual MSAA samples.");
|
|
29
|
+
if (backend.isWebGPUBackend) {
|
|
30
|
+
const limits = device?.limits;
|
|
31
|
+
if (!limits ||
|
|
32
|
+
limits.maxColorAttachments < 3 ||
|
|
33
|
+
limits.maxColorAttachmentBytesPerSample < 24 ||
|
|
34
|
+
Math.max(width, height) > limits.maxTextureDimension2D ||
|
|
35
|
+
![0, 1, 4].includes(samples))
|
|
36
|
+
fail("CAPABILITY", "Device cannot render three full-size rgba16float attachments at the current MSAA count.");
|
|
37
|
+
}
|
|
38
|
+
else if (backend.isWebGLBackend) {
|
|
39
|
+
const gl = backend.gl ?? fail("CAPABILITY", "WebGL2 backend is not initialized.");
|
|
40
|
+
if (!gl.getExtension("EXT_color_buffer_float") ||
|
|
41
|
+
gl.getParameter(gl.MAX_COLOR_ATTACHMENTS) < 3 ||
|
|
42
|
+
gl.getParameter(gl.MAX_DRAW_BUFFERS) < 3 ||
|
|
43
|
+
Math.max(width, height) > gl.getParameter(gl.MAX_TEXTURE_SIZE) ||
|
|
44
|
+
Math.max(width, height) > gl.getParameter(gl.MAX_RENDERBUFFER_SIZE))
|
|
45
|
+
fail("CAPABILITY", "WebGL2 requires EXT_color_buffer_float and three full-size rgba16float attachments.");
|
|
46
|
+
if (samples > 1 &&
|
|
47
|
+
!gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA16F, gl.SAMPLES).includes(samples))
|
|
48
|
+
fail("CAPABILITY", "WebGL2 cannot preserve the current MSAA count for rgba16float.");
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
fail("CAPABILITY", "An initialized Three WebGPU or WebGL2 backend is required.");
|
|
52
|
+
const ids = new Set();
|
|
53
|
+
const allowed = new Set(["uv", "screenSize", "texelSize"]);
|
|
54
|
+
for (const d of definitions) {
|
|
55
|
+
if (!d.id || d.id.startsWith("scene.") || ids.has(d.id))
|
|
56
|
+
fail("GRAPH", "Duplicate or reserved pass ID: " + d.id);
|
|
57
|
+
ids.add(d.id);
|
|
58
|
+
}
|
|
59
|
+
const earlier = new Set(["scene.color", "scene.normal", "scene.depth"]);
|
|
60
|
+
for (const d of definitions) {
|
|
61
|
+
if (d.graph.outputType !== "vec4")
|
|
62
|
+
fail("GRAPH", d.id + " must output vec4.");
|
|
63
|
+
for (const name of d.graph.requiredInputs ?? [])
|
|
64
|
+
if (!allowed.has(name))
|
|
65
|
+
fail("GRAPH", "Unsupported input: " + name);
|
|
66
|
+
// Check nodes as well as metadata so material inputs cannot leak in.
|
|
67
|
+
for (const n of d.graph.nodes) {
|
|
68
|
+
if (n.op === "input" && !allowed.has(n.name))
|
|
69
|
+
fail("GRAPH", "Unsupported input: " + n.name);
|
|
70
|
+
if (n.op === "readBuffer" && !Object.hasOwn(d.graph.bufferTypes ?? {}, n.buffer))
|
|
71
|
+
fail("GRAPH", "Buffer reads require a declared postprocess slot.");
|
|
72
|
+
if (n.op === "sampleFrame" && !earlier.has(n.channel))
|
|
73
|
+
fail("GRAPH", "Forward or unknown frame channel: " + n.channel);
|
|
74
|
+
if (n.op === "sampleHistory" && !ids.has(n.channel))
|
|
75
|
+
fail("GRAPH", "Unknown history pass: " + n.channel);
|
|
76
|
+
}
|
|
77
|
+
earlier.add(d.id);
|
|
78
|
+
}
|
|
79
|
+
let disposed = false, allocated = 0, released = 0, frames = 0, failures = 0;
|
|
80
|
+
let resets = 0, resetReason = "initial", lastMode;
|
|
81
|
+
const resources = [];
|
|
82
|
+
const own = (value) => {
|
|
83
|
+
resources.push(value);
|
|
84
|
+
allocated++;
|
|
85
|
+
return value;
|
|
86
|
+
};
|
|
87
|
+
const release = () => {
|
|
88
|
+
for (const resource of resources.splice(0).reverse()) {
|
|
89
|
+
resource.dispose();
|
|
90
|
+
released++;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const active = () => {
|
|
94
|
+
if (disposed)
|
|
95
|
+
fail("DISPOSED", "Composer is disposed.");
|
|
96
|
+
if (renderer.backend !== backend ||
|
|
97
|
+
(backend.isWebGPUBackend && backend.device !== device))
|
|
98
|
+
fail("CAPABILITY", "Renderer device changed; dispose and recreate the composer.");
|
|
99
|
+
};
|
|
100
|
+
const target = (name, count = 1, scene = false) => {
|
|
101
|
+
const t = own(new THREE.RenderTarget(width, height, {
|
|
102
|
+
count,
|
|
103
|
+
type: THREE.HalfFloatType,
|
|
104
|
+
format: THREE.RGBAFormat,
|
|
105
|
+
colorSpace: THREE.LinearSRGBColorSpace,
|
|
106
|
+
depthBuffer: scene,
|
|
107
|
+
stencilBuffer: false,
|
|
108
|
+
samples: scene ? samples : 0,
|
|
109
|
+
minFilter: THREE.NearestFilter,
|
|
110
|
+
magFilter: THREE.NearestFilter,
|
|
111
|
+
generateMipmaps: false,
|
|
112
|
+
}));
|
|
113
|
+
t.texture.name = name;
|
|
114
|
+
return t;
|
|
115
|
+
};
|
|
116
|
+
const material = (name) => {
|
|
117
|
+
const m = own(new THREE.NodeMaterial());
|
|
118
|
+
m.name = name;
|
|
119
|
+
m.toneMapped = false;
|
|
120
|
+
m.depthTest = m.depthWrite = false;
|
|
121
|
+
m.blending = THREE.NoBlending;
|
|
122
|
+
return m;
|
|
123
|
+
};
|
|
124
|
+
const passes = [];
|
|
125
|
+
try {
|
|
126
|
+
const source = target("output", 3, true);
|
|
127
|
+
source.textures[1].name = "normal";
|
|
128
|
+
source.textures[2].name = "linearDepth";
|
|
129
|
+
// Three's generated sky mesh is a real mesh, but not a surface hit.
|
|
130
|
+
const isBackground = (builder) => builder.object.name === "Background.mesh" &&
|
|
131
|
+
builder.material.name === "Background.material";
|
|
132
|
+
const rasterMRT = TSL.mrt({
|
|
133
|
+
output: TSL.output,
|
|
134
|
+
normal: TSL.Fn((builder) => isBackground(builder) ? TSL.vec4(0) : TSL.vec4(TSL.normalWorld, 1))(),
|
|
135
|
+
// "depth" is reserved by NodeMaterial.setupDepth for hardware depth writes.
|
|
136
|
+
linearDepth: TSL.Fn((builder) => isBackground(builder)
|
|
137
|
+
? TSL.vec4(0)
|
|
138
|
+
: TSL.vec4(TSL.max(TSL.positionWorld.sub(TSL.cameraPosition).length(), 1e-7), 0, 0, 1))(),
|
|
139
|
+
});
|
|
140
|
+
rasterMRT.setClearColor("normal", 0, 0);
|
|
141
|
+
rasterMRT.setClearColor("linearDepth", 0, 0);
|
|
142
|
+
const zero = own(new THREE.DataTexture(new Uint16Array(4), 1, 1, THREE.RGBAFormat, THREE.HalfFloatType));
|
|
143
|
+
zero.colorSpace = THREE.LinearSRGBColorSpace;
|
|
144
|
+
zero.generateMipmaps = false;
|
|
145
|
+
zero.needsUpdate = true;
|
|
146
|
+
zero.name = "$postprocess.zero";
|
|
147
|
+
const timeUniform = TSL.uniform(0);
|
|
148
|
+
const makeUniform = (value, type) => {
|
|
149
|
+
const v = Array.isArray(value)
|
|
150
|
+
? type === "vec2"
|
|
151
|
+
? new THREE.Vector2().fromArray(value)
|
|
152
|
+
: type === "vec3"
|
|
153
|
+
? new THREE.Vector3().fromArray(value)
|
|
154
|
+
: new THREE.Vector4().fromArray(value)
|
|
155
|
+
: value;
|
|
156
|
+
return TSL.uniform(v, type);
|
|
157
|
+
};
|
|
158
|
+
for (const d of definitions) {
|
|
159
|
+
const p = {
|
|
160
|
+
definition: d,
|
|
161
|
+
targets: [target(d.id + ":0"), target(d.id + ":1")],
|
|
162
|
+
material: material("PostProcess:" + d.id),
|
|
163
|
+
parameters: new Map((d.graph.parameters ?? []).map((v) => [
|
|
164
|
+
v.name,
|
|
165
|
+
makeUniform(v.default, v.type),
|
|
166
|
+
])),
|
|
167
|
+
frame: [],
|
|
168
|
+
history: [],
|
|
169
|
+
read: 0,
|
|
170
|
+
valid: false,
|
|
171
|
+
enabled: d.enabled,
|
|
172
|
+
revision: undefined,
|
|
173
|
+
};
|
|
174
|
+
passes.push(p);
|
|
175
|
+
p.material.name = "$post." + d.id;
|
|
176
|
+
if (Object.keys(d.graph.bufferTypes ?? {}).length) {
|
|
177
|
+
if (!buffers)
|
|
178
|
+
throw new Error("TC_RENDER_BUFFER_CAPABILITY: Postprocess storage slots require a managed WebGPU bridge.");
|
|
179
|
+
buffers.prepare(p.material, d.graph.bufferTypes);
|
|
180
|
+
}
|
|
181
|
+
if (Object.keys(d.graph.textureSlots ?? {}).length) {
|
|
182
|
+
if (!managedTextures)
|
|
183
|
+
throw new Error("TC_RENDER_TEXTURE_CAPABILITY: Postprocess texture slots require a managed WebGPU bridge.");
|
|
184
|
+
managedTextures.prepare(p.material, d.graph.textureSlots);
|
|
185
|
+
}
|
|
186
|
+
const sample = (bindings, channel, uv) => {
|
|
187
|
+
const n = TSL.texture(zero, uv);
|
|
188
|
+
n.updateMatrix = false;
|
|
189
|
+
n.mergeable = false;
|
|
190
|
+
bindings.push({ channel, node: n });
|
|
191
|
+
return n.level(TSL.float(0));
|
|
192
|
+
};
|
|
193
|
+
p.material.fragmentNode = typedGraphToTSL(d.graph, timeUniform, p.parameters, textures, {
|
|
194
|
+
inputs: {
|
|
195
|
+
uv: TSL.uv(0),
|
|
196
|
+
screenSize: TSL.uniform(new THREE.Vector2(width, height)),
|
|
197
|
+
texelSize: TSL.uniform(new THREE.Vector2(1 / width, 1 / height)),
|
|
198
|
+
},
|
|
199
|
+
sampleFrame: (channel, uv) => sample(p.frame, channel, uv),
|
|
200
|
+
sampleHistory: (channel, uv) => sample(p.history, channel, uv),
|
|
201
|
+
readBuffer: (name, index, type) => {
|
|
202
|
+
if (!buffers)
|
|
203
|
+
throw new Error("TC_RENDER_BUFFER_CAPABILITY: Postprocess storage slots require a managed WebGPU bridge.");
|
|
204
|
+
return buffers.node(p.material, name, index, type, true);
|
|
205
|
+
},
|
|
206
|
+
sampleTextureSlot: (name, uv) => {
|
|
207
|
+
if (!managedTextures)
|
|
208
|
+
throw new Error("TC_RENDER_TEXTURE_CAPABILITY: Postprocess texture slots require a managed WebGPU bridge.");
|
|
209
|
+
return managedTextures.node(p.material, name, uv, d.graph.textureSlots[name]);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const finalTexture = TSL.texture(source.texture, TSL.uv(0));
|
|
214
|
+
finalTexture.updateMatrix = false;
|
|
215
|
+
finalTexture.mergeable = false;
|
|
216
|
+
const finalMaterial = material("PostProcess:Output");
|
|
217
|
+
const traceExposure = TSL.uniform(1);
|
|
218
|
+
const quad = new THREE.QuadMesh(finalMaterial);
|
|
219
|
+
let finalKey = "";
|
|
220
|
+
// Scope all mutations, including QuadMesh.render's non-finally vertexNode assignment.
|
|
221
|
+
const scoped = (run) => {
|
|
222
|
+
const state = {
|
|
223
|
+
target: renderer.getRenderTarget(),
|
|
224
|
+
face: renderer.getActiveCubeFace(),
|
|
225
|
+
mip: renderer.getActiveMipmapLevel(),
|
|
226
|
+
mrt: renderer.getMRT(),
|
|
227
|
+
clear: renderer.getClearColor(new THREE.Color()),
|
|
228
|
+
alpha: renderer.getClearAlpha(),
|
|
229
|
+
depth: renderer.reversedDepthBuffer
|
|
230
|
+
? 1 - renderer.getClearDepth()
|
|
231
|
+
: renderer.getClearDepth(),
|
|
232
|
+
stencil: renderer.getClearStencil(),
|
|
233
|
+
viewport: renderer.getViewport(new ViewportSnapshot()),
|
|
234
|
+
scissor: renderer.getScissor(new THREE.Vector4()),
|
|
235
|
+
scissorTest: renderer.getScissorTest(),
|
|
236
|
+
auto: renderer.autoClear,
|
|
237
|
+
color: renderer.autoClearColor,
|
|
238
|
+
autoDepth: renderer.autoClearDepth,
|
|
239
|
+
autoStencil: renderer.autoClearStencil,
|
|
240
|
+
tone: renderer.toneMapping,
|
|
241
|
+
exposure: renderer.toneMappingExposure,
|
|
242
|
+
space: renderer.outputColorSpace,
|
|
243
|
+
xr: renderer.xr.enabled,
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
renderer.xr.enabled = false;
|
|
247
|
+
renderer.toneMapping = THREE.NoToneMapping;
|
|
248
|
+
renderer.toneMappingExposure = 1;
|
|
249
|
+
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
|
|
250
|
+
renderer.setScissorTest(false);
|
|
251
|
+
renderer.autoClear =
|
|
252
|
+
renderer.autoClearColor =
|
|
253
|
+
renderer.autoClearDepth =
|
|
254
|
+
true;
|
|
255
|
+
renderer.autoClearStencil = false;
|
|
256
|
+
run();
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
renderer.setRenderTarget(state.target, state.face, state.mip);
|
|
260
|
+
renderer.setMRT(state.mrt);
|
|
261
|
+
renderer.setClearColor(state.clear, state.alpha);
|
|
262
|
+
renderer.setClearDepth(state.depth);
|
|
263
|
+
renderer.setClearStencil(state.stencil);
|
|
264
|
+
renderer.setViewport(state.viewport.x, state.viewport.y, state.viewport.z, state.viewport.w, state.viewport.minDepth, state.viewport.maxDepth);
|
|
265
|
+
renderer.setScissor(state.scissor);
|
|
266
|
+
renderer.setScissorTest(state.scissorTest);
|
|
267
|
+
renderer.autoClear = state.auto;
|
|
268
|
+
renderer.autoClearColor = state.color;
|
|
269
|
+
renderer.autoClearDepth = state.autoDepth;
|
|
270
|
+
renderer.autoClearStencil = state.autoStencil;
|
|
271
|
+
renderer.toneMapping = state.tone;
|
|
272
|
+
renderer.toneMappingExposure = state.exposure;
|
|
273
|
+
renderer.outputColorSpace = state.space;
|
|
274
|
+
renderer.xr.enabled = state.xr;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const draw = (m) => {
|
|
278
|
+
quad.material = m;
|
|
279
|
+
const vertex = m.vertexNode;
|
|
280
|
+
try {
|
|
281
|
+
quad.render(renderer);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
m.vertexNode = vertex;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const resetHistory = (reason = "explicit") => {
|
|
288
|
+
active();
|
|
289
|
+
for (const p of passes)
|
|
290
|
+
p.valid = false;
|
|
291
|
+
resets++;
|
|
292
|
+
resetReason = reason;
|
|
293
|
+
};
|
|
294
|
+
return {
|
|
295
|
+
captureRaster(scene, camera) {
|
|
296
|
+
active();
|
|
297
|
+
scoped(() => {
|
|
298
|
+
renderer.setRenderTarget(source);
|
|
299
|
+
renderer.setMRT(rasterMRT);
|
|
300
|
+
renderer.setClearDepth(1);
|
|
301
|
+
renderer.render(scene, camera);
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
traceTargets() {
|
|
305
|
+
active();
|
|
306
|
+
if (!backend.isWebGPUBackend)
|
|
307
|
+
return fail("CAPABILITY", "Raw trace targets require WebGPU.");
|
|
308
|
+
let result;
|
|
309
|
+
scoped(() => {
|
|
310
|
+
renderer.initRenderTarget(source);
|
|
311
|
+
const [color, normal, depth] = source.textures.map((t) => backend.get(t).texture);
|
|
312
|
+
if (!color || !normal || !depth)
|
|
313
|
+
fail("CAPABILITY", "Three did not allocate the three native HDR attachments.");
|
|
314
|
+
result = { color: color, normal: normal, depth: depth };
|
|
315
|
+
});
|
|
316
|
+
return result;
|
|
317
|
+
},
|
|
318
|
+
present(owner, time, settings) {
|
|
319
|
+
active();
|
|
320
|
+
if (!Number.isFinite(Math.fround(time)))
|
|
321
|
+
fail("STATE", "Time must fit finite f32.");
|
|
322
|
+
const states = owner.states();
|
|
323
|
+
const byId = new Map(states.map((s) => [s.id, s]));
|
|
324
|
+
if (byId.size !== passes.length ||
|
|
325
|
+
states.length !== passes.length ||
|
|
326
|
+
passes.some((p) => !byId.has(p.definition.id)))
|
|
327
|
+
fail("STATE", "Owner pass IDs do not match the compiled definitions.");
|
|
328
|
+
const revisions = owner.historyRevisions();
|
|
329
|
+
if (lastMode !== undefined && lastMode !== settings.mode)
|
|
330
|
+
resetHistory("mode");
|
|
331
|
+
lastMode = settings.mode;
|
|
332
|
+
if (passes.some((p) => !p.enabled && byId.get(p.definition.id).enabled))
|
|
333
|
+
resetHistory("reenable");
|
|
334
|
+
for (const p of passes) {
|
|
335
|
+
const id = p.definition.id, revision = revisions[id];
|
|
336
|
+
if (!Number.isSafeInteger(revision) || revision < 0)
|
|
337
|
+
fail("STATE", "Missing history revision: " + id);
|
|
338
|
+
if (p.revision !== revision)
|
|
339
|
+
p.valid = false;
|
|
340
|
+
p.revision = revision;
|
|
341
|
+
p.enabled = byId.get(id).enabled;
|
|
342
|
+
}
|
|
343
|
+
timeUniform.value = time;
|
|
344
|
+
const savedTone = renderer.toneMapping, savedSpace = renderer.outputColorSpace;
|
|
345
|
+
try {
|
|
346
|
+
scoped(() => {
|
|
347
|
+
renderer.setMRT(null);
|
|
348
|
+
renderer.setClearColor(0, 0);
|
|
349
|
+
const channels = new Map([
|
|
350
|
+
["scene.color", source.textures[0]],
|
|
351
|
+
["scene.normal", source.textures[1]],
|
|
352
|
+
["scene.depth", source.textures[2]],
|
|
353
|
+
]);
|
|
354
|
+
let upstream = source.texture;
|
|
355
|
+
for (const p of passes) {
|
|
356
|
+
if (!p.enabled) {
|
|
357
|
+
channels.set(p.definition.id, upstream);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const output = p.targets[1 - p.read];
|
|
361
|
+
for (const b of p.frame) {
|
|
362
|
+
const t = channels.get(b.channel);
|
|
363
|
+
if (!t || t === output.texture)
|
|
364
|
+
fail("GRAPH", "Unsafe frame attachment: " + b.channel);
|
|
365
|
+
b.node.value = t;
|
|
366
|
+
}
|
|
367
|
+
for (const b of p.history) {
|
|
368
|
+
const previous = passes.find((v) => v.definition.id === b.channel);
|
|
369
|
+
const t = previous.valid
|
|
370
|
+
? previous.targets[previous.read].texture
|
|
371
|
+
: zero;
|
|
372
|
+
if (t === output.texture)
|
|
373
|
+
fail("GRAPH", "Unsafe history attachment: " + b.channel);
|
|
374
|
+
b.node.value = t;
|
|
375
|
+
}
|
|
376
|
+
const values = byId.get(p.definition.id).parameters;
|
|
377
|
+
for (const definition of p.definition.graph.parameters ?? []) {
|
|
378
|
+
const value = values[definition.name] ?? definition.default;
|
|
379
|
+
const uniform = p.parameters.get(definition.name);
|
|
380
|
+
const valid = definition.type === "bool"
|
|
381
|
+
? typeof value === "boolean"
|
|
382
|
+
: definition.type === "float"
|
|
383
|
+
? typeof value === "number" &&
|
|
384
|
+
Number.isFinite(Math.fround(value))
|
|
385
|
+
: Array.isArray(value) &&
|
|
386
|
+
value.length === Number(definition.type.slice(3)) &&
|
|
387
|
+
value.every((n) => typeof n === "number" &&
|
|
388
|
+
Number.isFinite(Math.fround(n)));
|
|
389
|
+
if (!valid)
|
|
390
|
+
fail("STATE", "Invalid parameter: " +
|
|
391
|
+
p.definition.id +
|
|
392
|
+
":" +
|
|
393
|
+
definition.name);
|
|
394
|
+
if (Array.isArray(value))
|
|
395
|
+
uniform.value.fromArray(value);
|
|
396
|
+
else
|
|
397
|
+
uniform.value = value;
|
|
398
|
+
}
|
|
399
|
+
renderer.setRenderTarget(output);
|
|
400
|
+
draw(p.material);
|
|
401
|
+
upstream = output.texture;
|
|
402
|
+
channels.set(p.definition.id, upstream);
|
|
403
|
+
}
|
|
404
|
+
finalTexture.value = upstream;
|
|
405
|
+
// Explicit output transform; automatic renderer transforms remain disabled.
|
|
406
|
+
const tracedBeauty = settings.mode !== "raster" && settings.debug === "beauty";
|
|
407
|
+
const rasterBeauty = settings.mode === "raster" && settings.debug === "beauty";
|
|
408
|
+
const tone = rasterBeauty ? savedTone : THREE.NoToneMapping;
|
|
409
|
+
const key = tone + ":" + savedSpace + ":" + tracedBeauty + ":" + rasterBeauty;
|
|
410
|
+
if (finalKey !== key) {
|
|
411
|
+
let color = finalTexture.level(TSL.float(0));
|
|
412
|
+
if (!rasterBeauty) {
|
|
413
|
+
const exposed = TSL.max(color.rgb.mul(traceExposure), 0);
|
|
414
|
+
// Match TRACE_PRESENT_SHADER, not Three's matrix-based ACES operator.
|
|
415
|
+
const rgb = tracedBeauty
|
|
416
|
+
? TSL.clamp(exposed
|
|
417
|
+
.mul(exposed.mul(2.51).add(0.03))
|
|
418
|
+
.div(exposed.mul(exposed.mul(2.43).add(0.59)).add(0.14)), 0, 1)
|
|
419
|
+
: exposed;
|
|
420
|
+
color = TSL.vec4(rgb, color.a);
|
|
421
|
+
}
|
|
422
|
+
finalMaterial.fragmentNode = TSL.renderOutput(color, tone, savedSpace);
|
|
423
|
+
finalMaterial.needsUpdate = true;
|
|
424
|
+
finalKey = key;
|
|
425
|
+
}
|
|
426
|
+
traceExposure.value = settings.exposure;
|
|
427
|
+
renderer.toneMappingExposure = rasterBeauty ? settings.exposure : 1;
|
|
428
|
+
renderer.setRenderTarget(null);
|
|
429
|
+
renderer.setViewport(0, 0, width / renderer.getPixelRatio(), height / renderer.getPixelRatio());
|
|
430
|
+
draw(finalMaterial);
|
|
431
|
+
});
|
|
432
|
+
// Commit only after the final draw AND state restoration succeeded.
|
|
433
|
+
for (const p of passes)
|
|
434
|
+
if (p.enabled) {
|
|
435
|
+
p.read = 1 - p.read;
|
|
436
|
+
p.valid = true;
|
|
437
|
+
}
|
|
438
|
+
frames++;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
failures++;
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
resetHistory,
|
|
446
|
+
dispose() {
|
|
447
|
+
if (disposed)
|
|
448
|
+
return;
|
|
449
|
+
disposed = true;
|
|
450
|
+
release();
|
|
451
|
+
},
|
|
452
|
+
inspect() {
|
|
453
|
+
return {
|
|
454
|
+
disposed,
|
|
455
|
+
width,
|
|
456
|
+
height,
|
|
457
|
+
samples,
|
|
458
|
+
format: "rgba16float",
|
|
459
|
+
sourceAttachments: ["output", "normal", "linearDepth"],
|
|
460
|
+
frames,
|
|
461
|
+
failures,
|
|
462
|
+
resets,
|
|
463
|
+
resetReason,
|
|
464
|
+
resources: {
|
|
465
|
+
allocated,
|
|
466
|
+
released,
|
|
467
|
+
live: allocated - released,
|
|
468
|
+
targets: disposed ? 0 : 1 + passes.length * 2,
|
|
469
|
+
textures: disposed ? 0 : 4 + passes.length * 2,
|
|
470
|
+
},
|
|
471
|
+
passes: passes.map((p) => ({
|
|
472
|
+
id: p.definition.id,
|
|
473
|
+
enabled: p.enabled,
|
|
474
|
+
historyValid: p.valid,
|
|
475
|
+
read: p.read,
|
|
476
|
+
revision: p.revision,
|
|
477
|
+
frameBindings: p.frame.map((b) => ({
|
|
478
|
+
channel: b.channel,
|
|
479
|
+
texture: b.node.value.name,
|
|
480
|
+
})),
|
|
481
|
+
historyBindings: p.history.map((b) => ({
|
|
482
|
+
channel: b.channel,
|
|
483
|
+
texture: b.node.value.name,
|
|
484
|
+
})),
|
|
485
|
+
})),
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
release();
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as THREE from "three/webgpu";
|
|
2
|
+
import * as TSL from "three/tsl";
|
|
3
|
+
const fail = (code, message) => { throw new Error("TC_RENDER_BUFFER_" + code + ": " + message + " Repair: Use a live matching WebGPU buffer binding and recreate resources after device replacement."); };
|
|
4
|
+
/** Three owns render attributes; the executor owns source buffers. Every update is a GPU copy. */
|
|
5
|
+
export function createRenderBufferBridge(renderer, source, resources) {
|
|
6
|
+
const backend = renderer.backend;
|
|
7
|
+
const device = backend.device;
|
|
8
|
+
const entries = new Map();
|
|
9
|
+
let disposed = false, copies = 0, copyBytes = 0, allocations = 0, released = 0;
|
|
10
|
+
const active = () => {
|
|
11
|
+
if (disposed)
|
|
12
|
+
fail("DISPOSED", "Render buffer bridge was disposed.");
|
|
13
|
+
if (renderer.backend !== backend || backend.isWebGLBackend || !device || backend.device !== device)
|
|
14
|
+
fail("CAPABILITY", "Managed GPU render slots require the initialized WebGPU backend.");
|
|
15
|
+
};
|
|
16
|
+
const disposeAttribute = (attribute) => {
|
|
17
|
+
attribute.dispose();
|
|
18
|
+
// Before a material's first render, Three's Attributes owner has no listener yet.
|
|
19
|
+
if (backend.has(attribute))
|
|
20
|
+
backend.destroyAttribute(attribute);
|
|
21
|
+
released++;
|
|
22
|
+
};
|
|
23
|
+
const makeAttribute = (count) => {
|
|
24
|
+
if (!Number.isSafeInteger(count) || count < 1 || count * 16 > device.limits.maxStorageBufferBindingSize || count * 16 > device.limits.maxBufferSize)
|
|
25
|
+
fail("COUNT", "Render storage count exceeds actual GPU limits.");
|
|
26
|
+
const attribute = new THREE.StorageBufferAttribute(count, 4);
|
|
27
|
+
attribute.name = "Pelican controlled render slot";
|
|
28
|
+
try {
|
|
29
|
+
backend.createStorageAttribute(attribute);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
attribute.dispose();
|
|
33
|
+
if (backend.has(attribute))
|
|
34
|
+
backend.destroyAttribute(attribute);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
allocations++;
|
|
38
|
+
return attribute;
|
|
39
|
+
};
|
|
40
|
+
const get = (material, name, type) => {
|
|
41
|
+
active();
|
|
42
|
+
const descriptor = resources(material.name)[name] ?? fail("UNBOUND", material.name + ":" + name);
|
|
43
|
+
if (descriptor.type !== type)
|
|
44
|
+
fail("TYPE", material.name + ":" + name);
|
|
45
|
+
let slots = entries.get(material);
|
|
46
|
+
if (!slots) {
|
|
47
|
+
slots = new Map();
|
|
48
|
+
entries.set(material, slots);
|
|
49
|
+
}
|
|
50
|
+
let entry = slots.get(name);
|
|
51
|
+
if (!entry) {
|
|
52
|
+
const attribute = makeAttribute(descriptor.values.length);
|
|
53
|
+
entry = { material, name, type, count: descriptor.values.length, length: TSL.uniform(descriptor.values.length), attribute, node: TSL.storage(attribute, "vec4", descriptor.values.length).toReadOnly() };
|
|
54
|
+
slots.set(name, entry);
|
|
55
|
+
}
|
|
56
|
+
return entry;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
node(material, name, index, type, robust = false) {
|
|
60
|
+
const entry = get(material, name, type);
|
|
61
|
+
const value = entry.node.element(TSL.uint(robust ? TSL.clamp(index, 0, entry.length.sub(1)) : index));
|
|
62
|
+
const typed = type === "bool" ? value.x.notEqual(0) : type === "float" ? value.x : type === "vec2" ? value.xy : type === "vec3" ? value.xyz : value;
|
|
63
|
+
if (!robust)
|
|
64
|
+
return typed;
|
|
65
|
+
const valid = index.greaterThanEqual(0).and(index.lessThan(entry.length)).and(index.equal(index.floor()));
|
|
66
|
+
const zero = type === "bool" ? TSL.bool(false) : type === "float" ? TSL.float(0) : type === "vec2" ? TSL.vec2(0) : type === "vec3" ? TSL.vec3(0) : TSL.vec4(0);
|
|
67
|
+
return TSL.select(valid, typed, zero);
|
|
68
|
+
},
|
|
69
|
+
prepare(material, names) {
|
|
70
|
+
for (const [name, type] of Object.entries(names))
|
|
71
|
+
get(material, name, type);
|
|
72
|
+
},
|
|
73
|
+
update() {
|
|
74
|
+
if (!entries.size)
|
|
75
|
+
return;
|
|
76
|
+
active();
|
|
77
|
+
const executor = source() ?? fail("CAPABILITY", "No managed compute GPU owner is attached.");
|
|
78
|
+
const planned = [];
|
|
79
|
+
for (const [material, slots] of entries) {
|
|
80
|
+
const values = resources(material.name);
|
|
81
|
+
for (const entry of slots.values()) {
|
|
82
|
+
const descriptor = values[entry.name] ?? fail("UNBOUND", material.name + ":" + entry.name);
|
|
83
|
+
if (descriptor.type !== entry.type)
|
|
84
|
+
fail("TYPE", material.name + ":" + entry.name);
|
|
85
|
+
planned.push({ entry, descriptor });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const encoder = device.createCommandEncoder({ label: "Pelican compute-to-render copies" });
|
|
89
|
+
const committed = [];
|
|
90
|
+
for (const { entry, descriptor } of planned) {
|
|
91
|
+
if (entry.key === descriptor.key && entry.revision === descriptor.revision)
|
|
92
|
+
continue;
|
|
93
|
+
const view = executor.resident(descriptor);
|
|
94
|
+
if (view.type !== entry.type || view.count !== descriptor.values.length || view.revision !== descriptor.revision || !(view.buffer.usage & GPUBufferUsage.COPY_SRC))
|
|
95
|
+
fail("RESOURCE", "Executor returned a mismatched committed resource.");
|
|
96
|
+
if (entry.count !== view.count) {
|
|
97
|
+
const attribute = makeAttribute(view.count);
|
|
98
|
+
disposeAttribute(entry.attribute);
|
|
99
|
+
entry.attribute = attribute;
|
|
100
|
+
entry.count = view.count;
|
|
101
|
+
entry.length.value = view.count;
|
|
102
|
+
entry.node.value = attribute;
|
|
103
|
+
entry.node.bufferCount = view.count;
|
|
104
|
+
entry.material.needsUpdate = true;
|
|
105
|
+
}
|
|
106
|
+
const target = backend.get(entry.attribute).buffer ?? fail("RESOURCE", "Three did not allocate the destination render attribute.");
|
|
107
|
+
if (target === view.buffer)
|
|
108
|
+
fail("RESOURCE", "Compute source and Three render destination must have different owners.");
|
|
109
|
+
encoder.copyBufferToBuffer(view.buffer, 0, target, 0, view.count * 16);
|
|
110
|
+
committed.push({ entry, key: descriptor.key, revision: descriptor.revision, bytes: view.count * 16 });
|
|
111
|
+
}
|
|
112
|
+
if (committed.length)
|
|
113
|
+
device.queue.submit([encoder.finish()]);
|
|
114
|
+
for (const item of committed) {
|
|
115
|
+
item.entry.key = item.key;
|
|
116
|
+
item.entry.revision = item.revision;
|
|
117
|
+
copies++;
|
|
118
|
+
copyBytes += item.bytes;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
inspect() { return { disposed, slots: [...entries.values()].reduce((count, slots) => count + slots.size, 0), allocations, released, liveAttributes: allocations - released, gpuCopies: copies, gpuCopyBytes: copyBytes, cpuResultUploads: 0, bindings: [...entries.values()].flatMap(slots => [...slots.values()].map(entry => ({ material: entry.material.name, name: entry.name, type: entry.type, count: entry.count, key: entry.key, revision: entry.revision }))) }; },
|
|
122
|
+
dispose() {
|
|
123
|
+
if (disposed)
|
|
124
|
+
return;
|
|
125
|
+
disposed = true;
|
|
126
|
+
for (const slots of entries.values())
|
|
127
|
+
for (const entry of slots.values())
|
|
128
|
+
disposeAttribute(entry.attribute);
|
|
129
|
+
entries.clear();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|