@forgeax/engine-runtime 0.1.28 → 0.1.29
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/README.md +98 -0
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +480 -15
- package/dist/index.mjs.map +1 -1
- package/dist/scene-utils/mount-override-fold.d.ts +3 -1
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/package.json +37 -32
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
- package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
- package/src/__tests__/asset-registry-mounts.test.ts +174 -734
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
- package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
- package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
- package/src/__tests__/collect-scene-asset.test.ts +96 -113
- package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
- package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
- package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
- package/src/__tests__/collect-transient-skip.test.ts +20 -27
- package/src/__tests__/components.test-d.ts +17 -1
- package/src/__tests__/components.unit.test.ts +75 -27
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
- package/src/__tests__/feature-depth-material.fixture.ts +473 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
- package/src/__tests__/geometry.unit.test.ts +42 -43
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
- package/src/__tests__/handle-migration.test.ts +0 -1
- package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
- package/src/__tests__/instances.test-d.ts +13 -52
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
- package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
- package/src/__tests__/material-publication.browser.test.ts +29 -0
- package/src/__tests__/material-publication.commands.ts +23 -0
- package/src/__tests__/material-publication.dawn.test.ts +24 -0
- package/src/__tests__/material-publication.fixture.ts +349 -0
- package/src/__tests__/material-publication.server.ts +109 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
- package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
- package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
- package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
- package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
- package/src/__tests__/record-worldid-isolation.test.ts +47 -28
- package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
- package/src/__tests__/render-system-extract.test.ts +61 -0
- package/src/__tests__/render-system-extract.unit.test.ts +21 -7
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
- package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
- package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
- package/src/__tests__/render-system-mega.test.ts +50 -62
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
- package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
- package/src/__tests__/roundtrip-primitive.test.ts +24 -23
- package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
- package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
- package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
- package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
- package/src/__tests__/serialize-scene-asset.test.ts +36 -50
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
- package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
- package/src/__tests__/transient-fields-collect.test.ts +4 -4
- package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
- package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
- package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
- package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
- package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
- package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
- package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
- package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
- package/src/__tests__/world-scene-despawn.test.ts +8 -8
- package/src/__tests__/world-scene-instantiate.test.ts +23 -29
- package/src/__tests__/world-scene-nested.test.ts +90 -163
- package/src/__tests__/world-scene-override.test.ts +17 -16
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
- package/src/collect-scene-asset.ts +637 -19
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/index.ts +1 -0
- package/src/scene-utils/mount-override-fold.ts +5 -14
- package/src/__tests__/node-shims.d.ts +0 -31
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { createBoxGeometry } from '@forgeax/engine-geometry';
|
|
2
|
+
import { AssetGuid } from '@forgeax/engine-pack';
|
|
3
|
+
import type { Renderer, RenderFeature } from '@forgeax/engine-render';
|
|
4
|
+
import { ok } from '@forgeax/engine-types';
|
|
5
|
+
import { ParticleEffectPlayer, type ParticleRendererSourceV3 } from '@forgeax/engine-vfx';
|
|
6
|
+
import { cookParticleCodeEffect } from '@forgeax/engine-vfx-compiler';
|
|
7
|
+
import { createVfxRuntimeHost } from '@forgeax/engine-vfx-render';
|
|
8
|
+
import { buildEngineShaderManifest } from '@forgeax/engine-vite-plugin-shader';
|
|
9
|
+
import { expect } from 'vitest';
|
|
10
|
+
import { constructRuntimeRendererHost } from '../renderer-host';
|
|
11
|
+
import { frameRequest, preparedWorld } from './render-feature-prepared-graphics.fixture';
|
|
12
|
+
|
|
13
|
+
const manifest = await buildEngineShaderManifest();
|
|
14
|
+
const schema = [
|
|
15
|
+
{ name: 'tint', type: 'vec4' },
|
|
16
|
+
{ name: 'selector', type: 'u32' },
|
|
17
|
+
{ name: 'direction', type: 'i32' },
|
|
18
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const particleDepthLayouts = [
|
|
22
|
+
{ name: 'billboard-material-input-instance', location: 9, offset: 31, size: 35, mesh: false },
|
|
23
|
+
{
|
|
24
|
+
name: 'topology-segment-material-input-instance',
|
|
25
|
+
location: 4,
|
|
26
|
+
offset: 12,
|
|
27
|
+
size: 16,
|
|
28
|
+
mesh: false,
|
|
29
|
+
},
|
|
30
|
+
{ name: 'mesh-geometry-material-input-instance', location: 10, offset: 18, size: 22, mesh: true },
|
|
31
|
+
] as const;
|
|
32
|
+
export async function verifyFeatureDepthMaterial(
|
|
33
|
+
layout: (typeof particleDepthLayouts)[number] & {
|
|
34
|
+
readonly particleKind?: ParticleRendererSourceV3['kind'];
|
|
35
|
+
readonly view: boolean;
|
|
36
|
+
readonly missing: 'none' | 'material' | 'depth';
|
|
37
|
+
readonly inputLane?: number;
|
|
38
|
+
readonly allInputs?: boolean;
|
|
39
|
+
readonly writeDepth?: boolean;
|
|
40
|
+
},
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const { view, missing } = layout;
|
|
43
|
+
const inputLane = layout.inputLane ?? 0;
|
|
44
|
+
const identifier = `test::depth-material-${layout.name}-${view}-${missing}`;
|
|
45
|
+
const instance = new Float32Array(layout.size);
|
|
46
|
+
instance[layout.offset] = 0.5;
|
|
47
|
+
const source = `
|
|
48
|
+
${view ? 'struct View { matrix: mat4x4<f32> }; @group(0) @binding(0) var<uniform> view: View;' : ''}
|
|
49
|
+
@group(0) @binding(${view ? 1 : 0}) var scene_depth: texture_depth_2d;
|
|
50
|
+
struct Material { tint: vec4<f32>, selector: u32, direction: i32 };
|
|
51
|
+
@group(1) @binding(0) var<uniform> material: Material;
|
|
52
|
+
@group(1) @binding(1) var material_sampler: sampler;
|
|
53
|
+
@group(1) @binding(2) var material_texture: texture_2d<f32>;
|
|
54
|
+
struct Input { @location(${layout.location + (layout.allInputs ? 3 : inputLane)}) particle: vec4<f32>,
|
|
55
|
+
${layout.allInputs ? `@location(${layout.location}) first: vec4<f32>, @location(${layout.location + 1}) second: vec4<f32>, @location(${layout.location + 2}) third: vec4<f32>,` : ''}
|
|
56
|
+
${layout.particleKind === 'mesh' ? '@location(0) geometry: vec3<f32>,' : ''}
|
|
57
|
+
};
|
|
58
|
+
struct Output { @builtin(position) position: vec4<f32>, @location(0) factor: f32 };
|
|
59
|
+
@vertex fn vs_main(input: Input, @builtin(vertex_index) index: u32) -> Output {
|
|
60
|
+
let p = array<vec2<f32>, 3>(vec2(-1., -1.), vec2(3., -1.), vec2(-1., 3.));
|
|
61
|
+
let factor = ${layout.allInputs ? 'select(-1.0, input.particle.x, input.first.x == 3.0 && input.second.x == 1.0 && input.third.x == 2.0)' : 'input.particle.x'};
|
|
62
|
+
return Output(vec4(${layout.particleKind === 'mesh' ? 'input.geometry.xy * 2.0' : 'p[index % 3u]'}, 0.5, 1.), factor);
|
|
63
|
+
}
|
|
64
|
+
@fragment fn fs_main(input: Output) -> @location(0) vec4<f32> {
|
|
65
|
+
let depth = textureLoad(scene_depth, vec2<i32>(input.position.xy), 0);
|
|
66
|
+
let sampled = textureSampleLevel(material_texture, material_sampler, vec2(0.5), 0.);
|
|
67
|
+
if (material.selector != 3u || material.direction != -7) { return vec4(0., 1., 0., 1.); }
|
|
68
|
+
return vec4(material.tint.rgb * sampled.rgb * depth * input.factor, 1.);
|
|
69
|
+
}`;
|
|
70
|
+
const shaderManifestUrl = `data:application/json,${encodeURIComponent(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
...manifest,
|
|
73
|
+
materialShaders: [
|
|
74
|
+
...manifest.materialShaders,
|
|
75
|
+
...(layout.writeDepth
|
|
76
|
+
? [
|
|
77
|
+
{
|
|
78
|
+
identifier: `${identifier}-writer`,
|
|
79
|
+
sourcePath: `${identifier}-writer`,
|
|
80
|
+
composedWgsl: `@vertex fn vs_main(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
|
|
81
|
+
let p = array<vec2<f32>, 3>(vec2(-1., -1.), vec2(3., -1.), vec2(-1., 3.));
|
|
82
|
+
return vec4(p[index], 0.25, 1.);
|
|
83
|
+
}`,
|
|
84
|
+
paramSchema: '[]',
|
|
85
|
+
variants: [],
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
89
|
+
{
|
|
90
|
+
identifier,
|
|
91
|
+
sourcePath: identifier,
|
|
92
|
+
composedWgsl: source,
|
|
93
|
+
paramSchema: JSON.stringify(schema),
|
|
94
|
+
variants: [],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
}),
|
|
98
|
+
)}`;
|
|
99
|
+
const materialGuid = AssetGuid.random();
|
|
100
|
+
const meshGuid = AssetGuid.random();
|
|
101
|
+
const particleInputs = layout.allInputs
|
|
102
|
+
? ['heat', 'warm', 'hot', 'glow'].map((name, lane) => ({
|
|
103
|
+
name,
|
|
104
|
+
type: 'f32' as const,
|
|
105
|
+
visibility: 'fragment' as const,
|
|
106
|
+
lane,
|
|
107
|
+
}))
|
|
108
|
+
: [{ name: 'heat', type: 'f32' as const, visibility: 'fragment' as const, lane: inputLane }];
|
|
109
|
+
const vfx =
|
|
110
|
+
layout.particleKind === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: createVfxRuntimeHost({
|
|
113
|
+
camera: {
|
|
114
|
+
read: () => ({
|
|
115
|
+
position: new Float32Array([0, 0, 3]),
|
|
116
|
+
right: new Float32Array([1, 0, 0]),
|
|
117
|
+
up: new Float32Array([0, 1, 0]),
|
|
118
|
+
viewProjection: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const feature: RenderFeature<undefined> = {
|
|
123
|
+
identity: identifier,
|
|
124
|
+
extract: () => ok(undefined),
|
|
125
|
+
plan: (_, context) => {
|
|
126
|
+
const color = context.targets.find((target) => target.kind === 'color');
|
|
127
|
+
const depth = context.targets.find((target) => target.kind === 'depth');
|
|
128
|
+
if (color === undefined || depth === undefined) throw new Error('Missing scene targets');
|
|
129
|
+
return ok({
|
|
130
|
+
resources: [
|
|
131
|
+
...(layout.writeDepth
|
|
132
|
+
? [
|
|
133
|
+
{
|
|
134
|
+
kind: 'graphics-program' as const,
|
|
135
|
+
name: 'depth-writer',
|
|
136
|
+
program: {
|
|
137
|
+
shader: `${identifier}-writer`,
|
|
138
|
+
vertexLayout: 'none',
|
|
139
|
+
colorFormats: [],
|
|
140
|
+
depthFormat: depth.format,
|
|
141
|
+
sampleCount: depth.sampleCount,
|
|
142
|
+
renderState: {
|
|
143
|
+
depthWriteEnabled: true,
|
|
144
|
+
depthCompare: 'always' as const,
|
|
145
|
+
cullMode: 'none' as const,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
kind: 'graphics-bindings' as const,
|
|
151
|
+
name: 'depth-bindings',
|
|
152
|
+
program: 'depth-writer',
|
|
153
|
+
values: { group: 0 },
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
: []),
|
|
157
|
+
{
|
|
158
|
+
kind: 'graphics-program',
|
|
159
|
+
name: 'program',
|
|
160
|
+
program: {
|
|
161
|
+
shader: identifier,
|
|
162
|
+
vertexLayout: layout.name,
|
|
163
|
+
particleInputLanes: 1,
|
|
164
|
+
colorFormats: [color.format],
|
|
165
|
+
sampleCount: color.sampleCount,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
kind: 'graphics-bindings',
|
|
170
|
+
name: 'scene',
|
|
171
|
+
program: 'program',
|
|
172
|
+
values: { group: 0, sceneDepthBinding: view ? 1 : 0 },
|
|
173
|
+
...(missing === 'depth' ? {} : { logicalTargets: { sceneDepth: depth.name } }),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
kind: 'graphics-bindings',
|
|
177
|
+
name: 'material',
|
|
178
|
+
program: 'program',
|
|
179
|
+
values: { group: 1, material: { world: 0, guid: AssetGuid.format(materialGuid) } },
|
|
180
|
+
},
|
|
181
|
+
{ kind: 'vertex-data', name: 'instance', layout: layout.name, data: instance },
|
|
182
|
+
...(layout.mesh
|
|
183
|
+
? [
|
|
184
|
+
{
|
|
185
|
+
kind: 'vertex-data' as const,
|
|
186
|
+
name: 'geometry',
|
|
187
|
+
layout: layout.name,
|
|
188
|
+
data: new Float32Array(36),
|
|
189
|
+
},
|
|
190
|
+
]
|
|
191
|
+
: []),
|
|
192
|
+
],
|
|
193
|
+
passes: [
|
|
194
|
+
...(layout.writeDepth
|
|
195
|
+
? [
|
|
196
|
+
{
|
|
197
|
+
kind: 'raster' as const,
|
|
198
|
+
name: 'write-depth',
|
|
199
|
+
colorAttachments: [],
|
|
200
|
+
depthStencilAttachment: {
|
|
201
|
+
target: depth.name,
|
|
202
|
+
depthLoadOp: 'load' as const,
|
|
203
|
+
depthStoreOp: 'store' as const,
|
|
204
|
+
},
|
|
205
|
+
draws: [
|
|
206
|
+
{
|
|
207
|
+
program: 'depth-writer',
|
|
208
|
+
bindings: ['depth-bindings'],
|
|
209
|
+
vertexData: [],
|
|
210
|
+
vertexLayout: 'none' as const,
|
|
211
|
+
draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
: []),
|
|
217
|
+
{
|
|
218
|
+
kind: 'raster',
|
|
219
|
+
name: 'sample',
|
|
220
|
+
colorAttachments: [{ target: color.name, loadOp: 'load', storeOp: 'store' }],
|
|
221
|
+
sampledTargets: missing === 'depth' ? [] : [depth.name],
|
|
222
|
+
draws: [
|
|
223
|
+
{
|
|
224
|
+
program: 'program',
|
|
225
|
+
bindings: ['scene', 'material'],
|
|
226
|
+
vertexData: layout.mesh
|
|
227
|
+
? [
|
|
228
|
+
{ slot: 0, resource: 'geometry' },
|
|
229
|
+
{ slot: 1, resource: 'instance' },
|
|
230
|
+
]
|
|
231
|
+
: [{ slot: 0, resource: 'instance' }],
|
|
232
|
+
draw: { kind: 'draw', vertexCount: 3, instanceCount: 1 },
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
const particleFeature =
|
|
241
|
+
vfx === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: {
|
|
244
|
+
...vfx.feature,
|
|
245
|
+
plan: (
|
|
246
|
+
data: Parameters<typeof vfx.feature.plan>[0],
|
|
247
|
+
context: Parameters<typeof vfx.feature.plan>[1],
|
|
248
|
+
) =>
|
|
249
|
+
vfx.feature.plan(
|
|
250
|
+
data,
|
|
251
|
+
missing === 'depth'
|
|
252
|
+
? {
|
|
253
|
+
...context,
|
|
254
|
+
targets: context.targets.filter((target) => target.kind !== 'depth'),
|
|
255
|
+
}
|
|
256
|
+
: context,
|
|
257
|
+
),
|
|
258
|
+
};
|
|
259
|
+
let device: GPUDevice | undefined;
|
|
260
|
+
let target: GPUTexture | undefined;
|
|
261
|
+
const gpuErrors: string[] = [];
|
|
262
|
+
const canvas = {
|
|
263
|
+
width: 64,
|
|
264
|
+
height: 64,
|
|
265
|
+
getContext: () => ({
|
|
266
|
+
configure(descriptor: { device: GPUDevice; format: GPUTextureFormat }) {
|
|
267
|
+
device = descriptor.device;
|
|
268
|
+
device.addEventListener('uncapturederror', (event) => gpuErrors.push(event.error.message));
|
|
269
|
+
target = device.createTexture({
|
|
270
|
+
size: [64, 64],
|
|
271
|
+
format: descriptor.format,
|
|
272
|
+
viewFormats: ['rgba8unorm-srgb'],
|
|
273
|
+
usage: 0x10 | 0x01,
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
unconfigure() {},
|
|
277
|
+
getCurrentTexture: () => target,
|
|
278
|
+
}),
|
|
279
|
+
addEventListener() {},
|
|
280
|
+
removeEventListener() {},
|
|
281
|
+
};
|
|
282
|
+
let renderer: Renderer | undefined;
|
|
283
|
+
let attachedWorld: ReturnType<typeof preparedWorld> | undefined;
|
|
284
|
+
try {
|
|
285
|
+
const constructed = await constructRuntimeRendererHost(
|
|
286
|
+
canvas,
|
|
287
|
+
{ features: [particleFeature ?? feature] },
|
|
288
|
+
{ shaderManifestUrl },
|
|
289
|
+
);
|
|
290
|
+
if (!constructed.ok) throw constructed.error;
|
|
291
|
+
const host = constructed.value;
|
|
292
|
+
renderer = host.renderer;
|
|
293
|
+
const textureGuid = AssetGuid.random();
|
|
294
|
+
if (vfx !== undefined) host.assets.catalog(meshGuid, createBoxGeometry(1, 1, 1).unwrap());
|
|
295
|
+
host.assets.catalog(textureGuid, {
|
|
296
|
+
kind: 'texture',
|
|
297
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
298
|
+
format: 'rgba8unorm',
|
|
299
|
+
data: new Uint8Array([255, 0, 0, 255]),
|
|
300
|
+
colorSpace: 'linear',
|
|
301
|
+
mips: { kind: 'none' },
|
|
302
|
+
});
|
|
303
|
+
if (missing !== 'material')
|
|
304
|
+
host.assets.catalog(materialGuid, {
|
|
305
|
+
kind: 'material',
|
|
306
|
+
passes: [
|
|
307
|
+
{
|
|
308
|
+
name: layout.particleKind === undefined ? 'Forward' : `particle-${layout.particleKind}`,
|
|
309
|
+
program: { module: identifier },
|
|
310
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 3000, depthWriteEnabled: false },
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
...(vfx === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: {
|
|
316
|
+
parameters: [
|
|
317
|
+
{ name: 'tint', type: 'vec4' },
|
|
318
|
+
{ name: 'selector', type: 'u32' },
|
|
319
|
+
{ name: 'direction', type: 'i32' },
|
|
320
|
+
{ name: 'baseColorTexture', type: 'texture' },
|
|
321
|
+
],
|
|
322
|
+
particleInputs,
|
|
323
|
+
}),
|
|
324
|
+
values: {
|
|
325
|
+
tint: [0.5, 1, 1, 1],
|
|
326
|
+
selector: 3,
|
|
327
|
+
direction: -7,
|
|
328
|
+
baseColorTexture: { texture: AssetGuid.format(textureGuid) as never },
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
const errors: unknown[] = [];
|
|
332
|
+
renderer.subscribe((event) => {
|
|
333
|
+
if (event.kind === 'error') errors.push(event.error);
|
|
334
|
+
});
|
|
335
|
+
const world = preparedWorld();
|
|
336
|
+
const attached = renderer.attach(world);
|
|
337
|
+
if (!attached.ok) throw attached.error;
|
|
338
|
+
const lease = attached.value;
|
|
339
|
+
if (vfx !== undefined) {
|
|
340
|
+
const particleRenderer = {
|
|
341
|
+
kind: layout.particleKind,
|
|
342
|
+
material: AssetGuid.format(materialGuid),
|
|
343
|
+
materialInputs: particleInputs.map((input) => input.name),
|
|
344
|
+
...(layout.particleKind === 'mesh' ? { mesh: AssetGuid.format(meshGuid) } : {}),
|
|
345
|
+
...(layout.particleKind === 'ribbon' ? { stripKey: 'alive-index', capacity: 4 } : {}),
|
|
346
|
+
...(layout.particleKind === 'trail' ? { historyLength: 3, capacity: 4 } : {}),
|
|
347
|
+
...(layout.particleKind === 'beam' ? { endpointField: 'velocity', capacity: 4 } : {}),
|
|
348
|
+
} as ParticleRendererSourceV3;
|
|
349
|
+
const cooked = await cookParticleCodeEffect(
|
|
350
|
+
{
|
|
351
|
+
schemaVersion: 3,
|
|
352
|
+
emitters: [
|
|
353
|
+
{
|
|
354
|
+
id: 'combined',
|
|
355
|
+
capacity: 4,
|
|
356
|
+
backend: { required: 'gpu' },
|
|
357
|
+
space: 'world',
|
|
358
|
+
bounds: { kind: 'sphere', center: [0, 0, 0], radius: 4 },
|
|
359
|
+
schedule: { rate: 0, bursts: [{ time: 0, count: 2 }] },
|
|
360
|
+
program: { module: 'combined.wgsl' },
|
|
361
|
+
renderers: [particleRenderer],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
'combined.wgsl': {
|
|
367
|
+
entry: `
|
|
368
|
+
#import forgeax_vfx::prelude::{VfxParticle, VfxSpawnContext, VfxUpdateContext}
|
|
369
|
+
struct VfxParameters { heat: f32, }
|
|
370
|
+
struct VfxCustom { heat: f32, ${layout.allInputs ? 'warm: f32, hot: f32, glow: f32,' : ''} }
|
|
371
|
+
fn vfx_spawn(ctx: VfxSpawnContext, particle: ptr<function, VfxParticle>, custom: ptr<function, VfxCustom>) {
|
|
372
|
+
(*particle).position = vec3<f32>(f32(ctx.particleId) * 0.1, 0.0, 0.0);
|
|
373
|
+
(*particle).velocity = vec3<f32>(0.0, 1.0, 0.0);
|
|
374
|
+
(*particle).mesh_orientation = vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
|
375
|
+
(*particle).mesh_scale = vec3<f32>(1.0);
|
|
376
|
+
(*particle).lifetime = 100.0;
|
|
377
|
+
}
|
|
378
|
+
fn vfx_update(ctx: VfxUpdateContext, particle: ptr<function, VfxParticle>, custom: ptr<function, VfxCustom>) {
|
|
379
|
+
(*particle).position.x += ctx.delta;
|
|
380
|
+
${layout.allInputs ? '(*custom).heat = 3.0; (*custom).warm = 1.0; (*custom).hot = 2.0; (*custom).glow = forgeax_vfx_parameters.heat;' : '(*custom).heat = forgeax_vfx_parameters.heat;'}
|
|
381
|
+
}`,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{ [AssetGuid.format(materialGuid)]: particleInputs },
|
|
385
|
+
);
|
|
386
|
+
if (!cooked.ok) throw cooked.error;
|
|
387
|
+
const attachment = await vfx.attachWorld({ world, assets: host.assets });
|
|
388
|
+
if (!attachment.ok) throw attachment.error;
|
|
389
|
+
attachedWorld = world;
|
|
390
|
+
const player = world
|
|
391
|
+
.spawn({
|
|
392
|
+
component: ParticleEffectPlayer,
|
|
393
|
+
data: {
|
|
394
|
+
effect: world.allocSharedRef('ParticleEffectAsset', cooked.value.asset),
|
|
395
|
+
playing: true,
|
|
396
|
+
seed: 1,
|
|
397
|
+
timeScale: 1,
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
.unwrap();
|
|
401
|
+
world.update(1 / 60).unwrap();
|
|
402
|
+
const control = vfx.acquireControl(world).unwrap();
|
|
403
|
+
const patched = control.patchPlayerParameters({ player, values: { heat: 0.5 } });
|
|
404
|
+
if (!patched.ok) throw patched.error;
|
|
405
|
+
}
|
|
406
|
+
for (let frame = 0; frame < 5; frame += 1) {
|
|
407
|
+
expect(world.update(1 / 60).ok).toBe(true);
|
|
408
|
+
const submitted = renderer.draw(frameRequest(lease));
|
|
409
|
+
if (!submitted.ok) throw submitted.error;
|
|
410
|
+
const observed = await renderer.observe(submitted.value, { include: ['draws'] });
|
|
411
|
+
if (!observed.ok) throw observed.error;
|
|
412
|
+
}
|
|
413
|
+
expect(gpuErrors).toEqual([]);
|
|
414
|
+
if (missing !== 'none') {
|
|
415
|
+
expect(errors).toEqual(
|
|
416
|
+
expect.arrayContaining([
|
|
417
|
+
expect.objectContaining({
|
|
418
|
+
detail: expect.objectContaining({
|
|
419
|
+
cause: expect.objectContaining({
|
|
420
|
+
code:
|
|
421
|
+
vfx !== undefined && missing === 'material'
|
|
422
|
+
? 'render-feature-stage-failed'
|
|
423
|
+
: 'render-feature-preparation-failed',
|
|
424
|
+
}),
|
|
425
|
+
}),
|
|
426
|
+
}),
|
|
427
|
+
]),
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
expect(errors).toEqual([]);
|
|
432
|
+
if (device === undefined || target === undefined) throw new Error('Missing GPU surface');
|
|
433
|
+
const readback = device.createBuffer({ size: 256 * 64, usage: 0x01 | 0x08 });
|
|
434
|
+
const encoder = device.createCommandEncoder();
|
|
435
|
+
encoder.copyTextureToBuffer(
|
|
436
|
+
{ texture: target },
|
|
437
|
+
{ buffer: readback, bytesPerRow: 256 },
|
|
438
|
+
[64, 64],
|
|
439
|
+
);
|
|
440
|
+
device.queue.submit([encoder.finish()]);
|
|
441
|
+
await readback.mapAsync(1);
|
|
442
|
+
const pixel = new Uint8Array(readback.getMappedRange()).slice(
|
|
443
|
+
32 * 256 + 32 * 4,
|
|
444
|
+
32 * 256 + 32 * 4 + 4,
|
|
445
|
+
);
|
|
446
|
+
readback.unmap();
|
|
447
|
+
readback.destroy();
|
|
448
|
+
if (layout.writeDepth) {
|
|
449
|
+
// Tint 0.5 * red texture 1 * instance 0.5 * written depth 0.25,
|
|
450
|
+
// encoded into the sRGB display surface. Clear depth 1 cannot pass.
|
|
451
|
+
const linearRed = 0.5 * 0.5 * 0.25;
|
|
452
|
+
const encodedRed = Math.round((1.055 * linearRed ** (1 / 2.4) - 0.055) * 255);
|
|
453
|
+
expect(Math.abs((pixel[0] ?? 0) - encodedRed)).toBeLessThanOrEqual(2);
|
|
454
|
+
} else expect(pixel[0]).toBeGreaterThan(50);
|
|
455
|
+
expect(pixel[1]).toBeLessThan(5);
|
|
456
|
+
expect(pixel[2]).toBeLessThan(5);
|
|
457
|
+
} finally {
|
|
458
|
+
// Negative material/depth cases return before a readback fence. Drain their
|
|
459
|
+
// submitted work before releasing this headless host's GPU resources.
|
|
460
|
+
if (device !== undefined) await device.queue.onSubmittedWorkDone();
|
|
461
|
+
if (vfx !== undefined && attachedWorld !== undefined)
|
|
462
|
+
await vfx.detachWorld({ world: attachedWorld });
|
|
463
|
+
renderer?.dispose();
|
|
464
|
+
target?.destroy();
|
|
465
|
+
// This headless fixture owns the raw device. Renderer.dispose releases
|
|
466
|
+
// renderer resources but deliberately leaves device lifetime to its host.
|
|
467
|
+
// Finish this case's device lifetime before starting the next combination.
|
|
468
|
+
if (device !== undefined) {
|
|
469
|
+
device.destroy();
|
|
470
|
+
expect((await device.lost).reason).toBe('destroyed');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -581,21 +581,21 @@ describe('feat-20260604 M2 w10: FullscreenPostProcessPass dawn tests', () => {
|
|
|
581
581
|
rgbByteDiffCount,
|
|
582
582
|
`paired readback must expose the existing FXAA difference; diagnostics=${JSON.stringify(diagnostics)}; ` +
|
|
583
583
|
`noneSurface=${JSON.stringify({
|
|
584
|
-
displayEncoded: inspectionNone.displayEncoded,
|
|
585
|
-
intermediateFormat: inspectionNone.intermediateFormat,
|
|
586
|
-
surfaceStorage: inspectionNone.surfaceStorage,
|
|
587
|
-
surfaceDisplay: inspectionNone.surfaceDisplay,
|
|
588
|
-
endpoint: inspectionNone.endpoint,
|
|
589
|
-
capability: inspectionNone.capability,
|
|
590
|
-
error: inspectionNone.error,
|
|
584
|
+
displayEncoded: inspectionNone.output.displayEncoded,
|
|
585
|
+
intermediateFormat: inspectionNone.output.intermediateFormat,
|
|
586
|
+
surfaceStorage: inspectionNone.output.surfaceStorage,
|
|
587
|
+
surfaceDisplay: inspectionNone.output.surfaceDisplay,
|
|
588
|
+
endpoint: inspectionNone.output.endpoint,
|
|
589
|
+
capability: inspectionNone.output.capability,
|
|
590
|
+
error: inspectionNone.output.error,
|
|
591
591
|
})}; fxaaSurface=${JSON.stringify({
|
|
592
|
-
displayEncoded: inspectionFxaa.displayEncoded,
|
|
593
|
-
intermediateFormat: inspectionFxaa.intermediateFormat,
|
|
594
|
-
surfaceStorage: inspectionFxaa.surfaceStorage,
|
|
595
|
-
surfaceDisplay: inspectionFxaa.surfaceDisplay,
|
|
596
|
-
endpoint: inspectionFxaa.endpoint,
|
|
597
|
-
capability: inspectionFxaa.capability,
|
|
598
|
-
error: inspectionFxaa.error,
|
|
592
|
+
displayEncoded: inspectionFxaa.output.displayEncoded,
|
|
593
|
+
intermediateFormat: inspectionFxaa.output.intermediateFormat,
|
|
594
|
+
surfaceStorage: inspectionFxaa.output.surfaceStorage,
|
|
595
|
+
surfaceDisplay: inspectionFxaa.output.surfaceDisplay,
|
|
596
|
+
endpoint: inspectionFxaa.output.endpoint,
|
|
597
|
+
capability: inspectionFxaa.output.capability,
|
|
598
|
+
error: inspectionFxaa.output.error,
|
|
599
599
|
standardOutputNone,
|
|
600
600
|
standardOutputFxaa,
|
|
601
601
|
})}`,
|
|
@@ -51,6 +51,7 @@ import { MeshRenderer } from '../../../render/src/components/mesh-renderer';
|
|
|
51
51
|
import type { MeshGpuHandles } from '../../../render/src/device/gpu-residency';
|
|
52
52
|
import { GpuResidencyCache } from '../../../render/src/device/gpu-residency';
|
|
53
53
|
import type { GpuBuffer } from '../../../render/src/gpu-resource';
|
|
54
|
+
import { InstanceCollectionStore } from '../../../render/src/instances';
|
|
54
55
|
import type {
|
|
55
56
|
PipelineBuilderContext,
|
|
56
57
|
PipelineBuilderShaderModuleFactory,
|
|
@@ -380,7 +381,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
380
381
|
kind: 'mesh',
|
|
381
382
|
vertices,
|
|
382
383
|
indices: new Uint16Array([0, 1, 2]),
|
|
383
|
-
attributes: { position: positions },
|
|
384
|
+
attributes: { ...buildMeshAttributeMapForUvSets(1), position: positions },
|
|
384
385
|
aabb,
|
|
385
386
|
materialSlots: [{ slotName: 'Default' }],
|
|
386
387
|
submeshes: [
|
|
@@ -683,23 +684,10 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
683
684
|
|
|
684
685
|
{
|
|
685
686
|
// --- from instances.test.ts ---
|
|
686
|
-
//
|
|
687
|
+
// Renderer-owned Instances association (E1 large-instance cutover).
|
|
687
688
|
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
// contract (length must be a non-zero multiple of 16) lives at the
|
|
691
|
-
// RenderSystem extract entry (`packages/runtime/src/render-system-extract.ts`)
|
|
692
|
-
// + the AI user set-site -- the ECS layer is stride-agnostic
|
|
693
|
-
// post-feat-20260515-buffer-array-vocab-collapse (decision §2.3).
|
|
694
|
-
//
|
|
695
|
-
// feat-20260515-buffer-array-vocab-collapse M3 / w16: the legacy
|
|
696
|
-
// component-level per-component stride defineComponent option was retired;
|
|
697
|
-
// stride violations now route the runtime-side
|
|
698
|
-
// `instance-transforms-stride-mismatch` error through the Layer-3
|
|
699
|
-
// ErrorHandler from the RenderSystem extract entry rather than from the
|
|
700
|
-
// ECS write paths. The runtime defensive coverage lives in
|
|
701
|
-
// `render-system-stride.test.ts` (w14); this file covers the ECS-side
|
|
702
|
-
// schema + happy-path read invariants only.
|
|
689
|
+
// ECS keeps only the collection identity; Render validates and owns the
|
|
690
|
+
// matrix payload, revision and dirty ranges in InstanceCollectionStore.
|
|
703
691
|
//
|
|
704
692
|
// Test path note: project convention places runtime tests under
|
|
705
693
|
// `src/__tests__/` (see `instances.test-d.ts` header for the rationale).
|
|
@@ -709,20 +697,25 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
709
697
|
readonly detail: unknown;
|
|
710
698
|
}
|
|
711
699
|
|
|
712
|
-
function makeHarness(): {
|
|
700
|
+
function makeHarness(): {
|
|
701
|
+
world: World;
|
|
702
|
+
collected: CollectedError[];
|
|
703
|
+
instanceCollections: InstanceCollectionStore;
|
|
704
|
+
} {
|
|
713
705
|
const collected: CollectedError[] = [];
|
|
714
706
|
const world = new World();
|
|
715
|
-
return { world, collected };
|
|
707
|
+
return { world, collected, instanceCollections: new InstanceCollectionStore() };
|
|
716
708
|
}
|
|
717
709
|
|
|
718
|
-
describe('
|
|
719
|
-
it('Instances
|
|
710
|
+
describe('renderer-owned Instances association', () => {
|
|
711
|
+
it('Instances schema carries only collectionId (no managed payload fields)', () => {
|
|
720
712
|
expect(Instances.name).toBe('Instances');
|
|
721
713
|
expect(Object.keys(Instances.fields).length).toBe(1);
|
|
722
|
-
expect(Instances.fields.
|
|
723
|
-
|
|
714
|
+
expect(Instances.fields.collectionId?.type).toBe('u32');
|
|
715
|
+
expect(Instances.fields.collectionId?.default).toBe(0);
|
|
724
716
|
expect(Instances.fields.buffer).toBeUndefined();
|
|
725
717
|
expect(Instances.fields.count).toBeUndefined();
|
|
718
|
+
expect(Instances.fields.transforms).toBeUndefined();
|
|
726
719
|
});
|
|
727
720
|
|
|
728
721
|
it('Instances component carries no per-component stride option (decision §2.3 migration)', () => {
|
|
@@ -732,8 +725,8 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
732
725
|
expect((Instances as unknown as Record<string, unknown>).arrayStride).toBeUndefined();
|
|
733
726
|
});
|
|
734
727
|
|
|
735
|
-
it('(legal)
|
|
736
|
-
const { world, collected } = makeHarness();
|
|
728
|
+
it('(legal) collection creation and ECS association preserve the matrix payload', () => {
|
|
729
|
+
const { world, collected, instanceCollections } = makeHarness();
|
|
737
730
|
const N = 4;
|
|
738
731
|
const transforms = new Float32Array(N * 16);
|
|
739
732
|
// Fill column 3 with distinguishable translations so the read path can
|
|
@@ -742,27 +735,29 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
742
735
|
transforms[i * 16 + 12] = i + 1;
|
|
743
736
|
transforms[i * 16 + 15] = 1;
|
|
744
737
|
}
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
738
|
+
const collection = instanceCollections.create({ transforms }).unwrap();
|
|
739
|
+
const e = world
|
|
740
|
+
.spawn({
|
|
741
|
+
component: Instances,
|
|
742
|
+
data: { collectionId: collection.collectionId },
|
|
743
|
+
})
|
|
744
|
+
.unwrap();
|
|
745
|
+
expect(collected).toHaveLength(0);
|
|
746
|
+
const association = world.get(e, Instances).unwrap();
|
|
747
|
+
expect(association.collectionId).toBe(collection.collectionId);
|
|
748
|
+
const snap = instanceCollections.snapshot(collection.collectionId).unwrap().transforms;
|
|
751
749
|
expect(snap.length).toBe(N * 16);
|
|
752
750
|
// Sanity: column 3 translation x of instance 2 reads back as 3.
|
|
753
751
|
expect(snap[2 * 16 + 12]).toBe(3);
|
|
754
752
|
});
|
|
755
753
|
|
|
756
|
-
it('
|
|
757
|
-
const { world, collected } = makeHarness();
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
);
|
|
764
|
-
// Negative anchor for the retired ECS-layer code.
|
|
765
|
-
expect(collected.filter((c) => c.code === 'managed-array-stride-mismatch').length).toBe(0);
|
|
754
|
+
it('rejects malformed matrix payloads before publishing an ECS association', () => {
|
|
755
|
+
const { world, collected, instanceCollections } = makeHarness();
|
|
756
|
+
const malformed = instanceCollections.create({ transforms: new Float32Array(17) });
|
|
757
|
+
expect(malformed.ok).toBe(false);
|
|
758
|
+
expect(collected).toHaveLength(0);
|
|
759
|
+
const e = world.spawn({ component: Instances, data: {} }).unwrap();
|
|
760
|
+
expect(world.get(e, Instances).unwrap().collectionId).toBe(0);
|
|
766
761
|
});
|
|
767
762
|
});
|
|
768
763
|
}
|
|
@@ -879,7 +874,6 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
879
874
|
iboBytes: 0,
|
|
880
875
|
indexCount: 0,
|
|
881
876
|
indexFormat: 'uint16',
|
|
882
|
-
layout: '12F',
|
|
883
877
|
vertexCount: 2,
|
|
884
878
|
indexed: false,
|
|
885
879
|
topology: 'line-list',
|
|
@@ -2341,7 +2335,11 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
2341
2335
|
{ component: C.MeshRenderer, data: { materials: materialHandles } },
|
|
2342
2336
|
{ component: C.MeshFilter, data: { assetHandle: meshHandle } },
|
|
2343
2337
|
{ component: C.Transform, data: originTransform() },
|
|
2344
|
-
{ component: C.
|
|
2338
|
+
{ component: C.GlobalTransform, data: {} },
|
|
2339
|
+
{
|
|
2340
|
+
component: C.Instances,
|
|
2341
|
+
data: { collectionId: _renderer.instances.create({ transforms }).unwrap().collectionId },
|
|
2342
|
+
},
|
|
2345
2343
|
);
|
|
2346
2344
|
return world;
|
|
2347
2345
|
}
|
|
@@ -2518,6 +2516,7 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
2518
2516
|
vertices: new Float32Array(4 * 12), // 4 verts x 12 floats
|
|
2519
2517
|
indices,
|
|
2520
2518
|
attributes: {
|
|
2519
|
+
...buildMeshAttributeMapForUvSets(1),
|
|
2521
2520
|
position: new Float32Array(4 * 3),
|
|
2522
2521
|
},
|
|
2523
2522
|
materialSlots: [{ slotName: 'Default' }],
|