@plasius/gpu-renderer 0.2.46 → 0.2.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +230 -0
- package/dist/index.cjs +512 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +510 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/canonical-wavefront-meshes.js +226 -0
- package/src/index.d.ts +55 -0
- package/src/index.js +3 -0
- package/src/native-scene-renderer.js +152 -7
- package/src/native-scene-shader.js +68 -18
- package/src/native-surface-materials.js +124 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/gpu-renderer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.48",
|
|
4
4
|
"description": "Framework-agnostic WebGPU renderer runtime for Plasius projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"license": "Apache-2.0",
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@plasius/gpu-camera": "^0.1.18",
|
|
56
|
+
"@plasius/gpu-model-core": "^0.4.4",
|
|
56
57
|
"@plasius/gpu-shared": "^1.1.1",
|
|
57
58
|
"@plasius/gpu-xr": "^0.1.17",
|
|
58
59
|
"@types/webxr": "^0.5.24"
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
}
|
|
89
90
|
],
|
|
90
91
|
"overrides": {
|
|
91
|
-
"brace-expansion": "5.0.
|
|
92
|
+
"brace-expansion": "5.0.12",
|
|
92
93
|
"esbuild": "^0.28.1",
|
|
93
94
|
"minimatch": "^10.2.1"
|
|
94
95
|
},
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const STANDARD_TEXTURE_BINDINGS = Object.freeze([
|
|
2
|
+
["baseColor", "baseColorTexture"],
|
|
3
|
+
["metallicRoughness", "metallicRoughnessTexture"],
|
|
4
|
+
["normal", "normalTexture"],
|
|
5
|
+
["occlusion", "occlusionTexture"],
|
|
6
|
+
["emissive", "emissiveTexture"],
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const EXTENSION_TEXTURE_BINDINGS = Object.freeze([
|
|
10
|
+
["clearcoat", "clearcoat", "clearcoatTexture"],
|
|
11
|
+
["clearcoatRoughness", "clearcoat", "clearcoatRoughnessTexture"],
|
|
12
|
+
["clearcoatNormal", "clearcoat", "clearcoatNormalTexture"],
|
|
13
|
+
["transmission", "transmission", "transmissionTexture"],
|
|
14
|
+
["sheenColor", "sheen", "sheenColorTexture"],
|
|
15
|
+
["sheenRoughness", "sheen", "sheenRoughnessTexture"],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function requireArray(value, label) {
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
throw new TypeError(`${label} must be an array.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireMap(value, label) {
|
|
26
|
+
if (!value || typeof value !== "object" || typeof value.get !== "function") {
|
|
27
|
+
throw new TypeError(`${label} must provide a ReadonlyMap-compatible get method.`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function textureSample(binding, textures, label, extra = {}) {
|
|
33
|
+
if (binding === undefined || binding === null) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sample = textures.get(binding.textureId);
|
|
38
|
+
if (!sample) {
|
|
39
|
+
throw new Error(`Missing decoded texture sample ${binding.textureId} for ${label}.`);
|
|
40
|
+
}
|
|
41
|
+
const width = Number(sample.width);
|
|
42
|
+
const height = Number(sample.height);
|
|
43
|
+
const data = sample.data;
|
|
44
|
+
const dataLength = data && typeof data.length === "number" ? data.length : -1;
|
|
45
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
46
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} has invalid dimensions.`);
|
|
47
|
+
}
|
|
48
|
+
if (width > 4_096 || height > 4_096 || width * height > 4_096 * 4_096) {
|
|
49
|
+
throw new RangeError(`Decoded texture sample ${binding.textureId} exceeds the 4096-pixel resource limit.`);
|
|
50
|
+
}
|
|
51
|
+
if (!(Array.isArray(data) || ArrayBuffer.isView(data)) || dataLength < width * height * 4) {
|
|
52
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} must contain width × height × 4 RGBA values.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...sample,
|
|
57
|
+
...(binding.texCoordSet === undefined ? {} : { texCoord: binding.texCoordSet }),
|
|
58
|
+
...extra,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cloneMaterialExtensions(material, textures) {
|
|
63
|
+
const extensions = { ...(material.extensions ?? {}) };
|
|
64
|
+
if (material.workflow === "unlit") {
|
|
65
|
+
const unlitKey = "KHR_materials_unlit";
|
|
66
|
+
const unlitValue = extensions[unlitKey] ?? extensions.unlit ?? {};
|
|
67
|
+
if (typeof unlitValue !== "object" || unlitValue === null || Array.isArray(unlitValue)) {
|
|
68
|
+
throw new TypeError(`Canonical material extension ${unlitKey} must be an object.`);
|
|
69
|
+
}
|
|
70
|
+
extensions[unlitKey] = { ...unlitValue, enabled: true };
|
|
71
|
+
}
|
|
72
|
+
for (const [bindingName, extensionName, textureKey] of EXTENSION_TEXTURE_BINDINGS) {
|
|
73
|
+
const binding = material.textures?.[bindingName];
|
|
74
|
+
if (!binding) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const extensionKey = `KHR_materials_${extensionName}`;
|
|
79
|
+
const extensionValue = extensions[extensionKey] ?? extensions[extensionName] ?? {};
|
|
80
|
+
if (typeof extensionValue !== "object" || extensionValue === null || Array.isArray(extensionValue)) {
|
|
81
|
+
throw new TypeError(`Canonical material extension ${extensionKey} must be an object.`);
|
|
82
|
+
}
|
|
83
|
+
extensions[extensionKey] = {
|
|
84
|
+
...extensionValue,
|
|
85
|
+
[textureKey]: textureSample(binding, textures, `${extensionKey}.${textureKey}`),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return extensions;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mapMaterial(material, materialRefId, textures) {
|
|
92
|
+
if (!material) {
|
|
93
|
+
return { materialRefId };
|
|
94
|
+
}
|
|
95
|
+
if (material.alphaMode === "mask") {
|
|
96
|
+
throw new Error("Canonical alphaMode mask is unsupported by WavefrontMeshInput.");
|
|
97
|
+
}
|
|
98
|
+
if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
|
|
99
|
+
throw new Error(`Unsupported canonical material workflow: ${material.workflow}.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const mapped = {
|
|
103
|
+
materialRefId,
|
|
104
|
+
...(material.alphaMode === "blend" ? { materialKind: "transparent" } : {}),
|
|
105
|
+
baseColor: material.baseColorFactor,
|
|
106
|
+
emission: material.emissiveFactor
|
|
107
|
+
? [...material.emissiveFactor, 1]
|
|
108
|
+
: [0, 0, 0, 1],
|
|
109
|
+
roughness: material.roughnessFactor,
|
|
110
|
+
metallic: material.metallicFactor,
|
|
111
|
+
opacity: material.alphaMode === "opaque" ? 1 : material.baseColorFactor?.[3],
|
|
112
|
+
clearcoat: material.clearcoatFactor,
|
|
113
|
+
clearcoatRoughness: material.clearcoatRoughnessFactor,
|
|
114
|
+
transmission: material.transmissionFactor,
|
|
115
|
+
sheenColor: material.sheenColorFactor,
|
|
116
|
+
extensions: cloneMaterialExtensions(material, textures),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const [bindingName, inputName] of STANDARD_TEXTURE_BINDINGS) {
|
|
120
|
+
const binding = material.textures?.[bindingName];
|
|
121
|
+
if (!binding) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const extra = bindingName === "normal" && material.normalScale !== undefined
|
|
125
|
+
? { scale: material.normalScale }
|
|
126
|
+
: bindingName === "occlusion" && material.occlusionStrength !== undefined
|
|
127
|
+
? { strength: material.occlusionStrength }
|
|
128
|
+
: {};
|
|
129
|
+
mapped[inputName] = textureSample(binding, textures, inputName, extra);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return mapped;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Maps decoded canonical mesh primitives into the renderer's existing Wavefront
|
|
137
|
+
* input contract. Geometry and image decoding remain upstream responsibilities.
|
|
138
|
+
*/
|
|
139
|
+
export function createCanonicalWavefrontMeshInputs({ document, geometry, textures = new Map() } = {}) {
|
|
140
|
+
if (!document || typeof document !== "object") {
|
|
141
|
+
throw new TypeError("Canonical model document is required.");
|
|
142
|
+
}
|
|
143
|
+
const meshes = requireArray(document.meshes, "Canonical model meshes");
|
|
144
|
+
const materials = requireArray(document.materials, "Canonical model materials");
|
|
145
|
+
const geometryInputs = requireArray(geometry, "Renderer-ready geometry");
|
|
146
|
+
const textureSamples = requireMap(textures, "Decoded texture samples");
|
|
147
|
+
|
|
148
|
+
const geometryByPrimitiveId = new Map();
|
|
149
|
+
for (const entry of geometryInputs) {
|
|
150
|
+
if (!entry || typeof entry.primitiveId !== "string" || entry.primitiveId.length === 0) {
|
|
151
|
+
throw new TypeError("Renderer-ready geometry requires a non-empty primitiveId.");
|
|
152
|
+
}
|
|
153
|
+
if (geometryByPrimitiveId.has(entry.primitiveId)) {
|
|
154
|
+
throw new Error(`Duplicate renderer-ready geometry for primitive ${entry.primitiveId}.`);
|
|
155
|
+
}
|
|
156
|
+
geometryByPrimitiveId.set(entry.primitiveId, entry);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const materialsById = new Map();
|
|
160
|
+
const materialRefIds = new Map();
|
|
161
|
+
materials.forEach((material, index) => {
|
|
162
|
+
if (!material || typeof material.id !== "string" || material.id.length === 0) {
|
|
163
|
+
throw new TypeError("Canonical materials require a non-empty id.");
|
|
164
|
+
}
|
|
165
|
+
if (materialsById.has(material.id)) {
|
|
166
|
+
throw new Error(`Duplicate canonical material ${material.id}.`);
|
|
167
|
+
}
|
|
168
|
+
materialsById.set(material.id, material);
|
|
169
|
+
materialRefIds.set(material.id, index + 1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const output = [];
|
|
173
|
+
const consumedPrimitiveIds = new Set();
|
|
174
|
+
const canonicalPrimitiveIds = new Set();
|
|
175
|
+
for (const mesh of meshes) {
|
|
176
|
+
if (!mesh || typeof mesh.id !== "string") {
|
|
177
|
+
throw new TypeError("Canonical meshes require a string id.");
|
|
178
|
+
}
|
|
179
|
+
for (const primitive of requireArray(mesh.primitives, `Primitives for mesh ${mesh.id}`)) {
|
|
180
|
+
if (!primitive || typeof primitive.id !== "string" || primitive.id.length === 0) {
|
|
181
|
+
throw new TypeError(`Canonical mesh ${mesh.id} contains a primitive without an id.`);
|
|
182
|
+
}
|
|
183
|
+
if (canonicalPrimitiveIds.has(primitive.id)) {
|
|
184
|
+
throw new Error(`Duplicate canonical primitive ${primitive.id}.`);
|
|
185
|
+
}
|
|
186
|
+
canonicalPrimitiveIds.add(primitive.id);
|
|
187
|
+
if (primitive.topology !== "triangles") {
|
|
188
|
+
throw new Error(`Unsupported canonical primitive topology: ${primitive.topology}.`);
|
|
189
|
+
}
|
|
190
|
+
const source = geometryByPrimitiveId.get(primitive.id);
|
|
191
|
+
if (!source) {
|
|
192
|
+
throw new Error(`Missing renderer-ready geometry for primitive ${primitive.id}.`);
|
|
193
|
+
}
|
|
194
|
+
consumedPrimitiveIds.add(primitive.id);
|
|
195
|
+
|
|
196
|
+
let material = null;
|
|
197
|
+
let materialRefId = 0;
|
|
198
|
+
if (primitive.materialId !== undefined) {
|
|
199
|
+
material = materialsById.get(primitive.materialId);
|
|
200
|
+
if (!material) {
|
|
201
|
+
throw new Error(`Missing canonical material ${primitive.materialId} for primitive ${primitive.id}.`);
|
|
202
|
+
}
|
|
203
|
+
materialRefId = materialRefIds.get(primitive.materialId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
output.push(Object.freeze({
|
|
207
|
+
id: output.length + 1,
|
|
208
|
+
positions: source.positions,
|
|
209
|
+
...(source.indices === undefined ? {} : { indices: source.indices }),
|
|
210
|
+
...(source.normals === undefined ? {} : { normals: source.normals }),
|
|
211
|
+
...((source.texcoords ?? source.uvs) === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { uvs: source.texcoords ?? source.uvs }),
|
|
214
|
+
...mapMaterial(material, materialRefId, textureSamples),
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const primitiveId of geometryByPrimitiveId.keys()) {
|
|
220
|
+
if (!consumedPrimitiveIds.has(primitiveId)) {
|
|
221
|
+
throw new Error(`Renderer-ready geometry references unknown primitive ${primitiveId}.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return Object.freeze(output);
|
|
226
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
FeedbackGameDiagnosticViewportBucket,
|
|
14
14
|
} from "@plasius/gpu-shared/feedback-diagnostics";
|
|
15
15
|
import type { XrStoreState } from "@plasius/gpu-xr";
|
|
16
|
+
import type { GpuModelDocument } from "@plasius/gpu-model-core";
|
|
16
17
|
|
|
17
18
|
export interface CreateFeedbackGameDiagnosticSnapshotInput {
|
|
18
19
|
readonly featureEnabled: boolean;
|
|
@@ -332,6 +333,30 @@ export interface WavefrontTextureSampleInput {
|
|
|
332
333
|
readonly data: Uint8ClampedArray | Uint8Array | readonly number[];
|
|
333
334
|
}
|
|
334
335
|
|
|
336
|
+
export interface CanonicalRendererGeometryInput {
|
|
337
|
+
/** Canonical primitive id whose geometry has already been decoded and validated. */
|
|
338
|
+
readonly primitiveId: string;
|
|
339
|
+
readonly positions: readonly number[] | Float32Array;
|
|
340
|
+
readonly indices?: readonly number[] | Uint16Array | Uint32Array;
|
|
341
|
+
readonly normals?: readonly number[] | Float32Array | null;
|
|
342
|
+
readonly texcoords?: readonly number[] | Float32Array | null;
|
|
343
|
+
readonly uvs?: readonly number[] | Float32Array | null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface CreateCanonicalWavefrontMeshInputsOptions {
|
|
347
|
+
/** Canonical model metadata. Parsing, resource resolution, and image decoding are upstream responsibilities. */
|
|
348
|
+
readonly document: Pick<GpuModelDocument, "meshes" | "materials">;
|
|
349
|
+
/** Renderer-ready, decoded geometry indexed by canonical primitive id. */
|
|
350
|
+
readonly geometry: readonly CanonicalRendererGeometryInput[];
|
|
351
|
+
/** Decoded image samples indexed by canonical texture id. */
|
|
352
|
+
readonly textures?: ReadonlyMap<string, WavefrontTextureSampleInput>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Maps canonical primitives and materials to the renderer's existing Wavefront input shape. */
|
|
356
|
+
export function createCanonicalWavefrontMeshInputs(
|
|
357
|
+
options: CreateCanonicalWavefrontMeshInputsOptions,
|
|
358
|
+
): readonly WavefrontMeshInput[];
|
|
359
|
+
|
|
335
360
|
export interface WavefrontMediumInput {
|
|
336
361
|
readonly id?: number;
|
|
337
362
|
readonly mediumId?: number;
|
|
@@ -1582,9 +1607,34 @@ export interface RendererSnapshot {
|
|
|
1582
1607
|
xrActive: boolean;
|
|
1583
1608
|
}
|
|
1584
1609
|
|
|
1610
|
+
/** Decoded RGBA8 image, matching gpu-shared glTF material textures. */
|
|
1611
|
+
export interface NativeSurfaceImage {
|
|
1612
|
+
width: number;
|
|
1613
|
+
height: number;
|
|
1614
|
+
data: Uint8Array | Uint8ClampedArray;
|
|
1615
|
+
}
|
|
1616
|
+
export interface NativeSurfaceMaterial {
|
|
1617
|
+
/** sRGB colour; RGB is multiplied by each vertex's linear base-colour factor. */
|
|
1618
|
+
baseColor?: NativeSurfaceImage;
|
|
1619
|
+
/** Linear, OpenGL-style tangent-space normal map. */
|
|
1620
|
+
normal?: NativeSurfaceImage;
|
|
1621
|
+
/** Linear R occlusion, G roughness, B metalness. */
|
|
1622
|
+
orm?: NativeSurfaceImage;
|
|
1623
|
+
normalScale?: number;
|
|
1624
|
+
/** Independent dielectric coating strength, 0–1; defaults to 0 (off). */
|
|
1625
|
+
clearcoat?: number;
|
|
1626
|
+
/** Coating roughness, 0–1; independent of the base map; defaults to 0. */
|
|
1627
|
+
clearcoatRoughness?: number;
|
|
1628
|
+
/** Linear RGBA8: R multiplies clearcoat, G multiplies clearcoatRoughness; B/A ignored. White when omitted. */
|
|
1629
|
+
clearcoatMap?: NativeSurfaceImage;
|
|
1630
|
+
}
|
|
1585
1631
|
/** Interleaved world-space triangles: position, normal, linear colour, roughness/metalness/detail. */
|
|
1586
1632
|
export interface NativeSceneFrame {
|
|
1587
1633
|
vertices: Float32Array;
|
|
1634
|
+
/** Two UV components per vertex. Required when surfaces are supplied. */
|
|
1635
|
+
texcoords?: Float32Array;
|
|
1636
|
+
/** Contiguous triangle ranges covering all vertices, using zero-based materials. */
|
|
1637
|
+
surfaces?: { firstVertex: number; vertexCount: number; materialIndex: number }[];
|
|
1588
1638
|
camera: { eye: [number, number, number]; target: [number, number, number]; fov?: number };
|
|
1589
1639
|
time?: number;
|
|
1590
1640
|
water?: boolean;
|
|
@@ -1605,6 +1655,8 @@ export function createNativeSceneRenderer(options: {
|
|
|
1605
1655
|
canvas: HTMLCanvasElement;
|
|
1606
1656
|
navigator?: Navigator | { gpu?: GPU };
|
|
1607
1657
|
maxVertices?: number;
|
|
1658
|
+
/** At most 16 materials, 64 MiB decoded RGBA8 total; maps are power-of-two, up to 2048. */
|
|
1659
|
+
materials?: NativeSurfaceMaterial[];
|
|
1608
1660
|
onUnavailable?: (message: string) => void;
|
|
1609
1661
|
}): Promise<{
|
|
1610
1662
|
render(frame: NativeSceneFrame): NativeSceneSnapshot;
|
|
@@ -2028,3 +2080,6 @@ export const rendererWorkerProfiles: Readonly<
|
|
|
2028
2080
|
export const rendererWorkerManifests: Readonly<
|
|
2029
2081
|
Record<RendererWorkerProfileName, RendererWorkerManifest>
|
|
2030
2082
|
>;
|
|
2083
|
+
|
|
2084
|
+
/** Validate native materials and bounded decoded maps without GPU allocation. */
|
|
2085
|
+
export declare function validateNativeSurfaceMaterials(materials: NativeSurfaceMaterial[]): void;
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createNativeSceneRenderer } from "./native-scene-renderer.js";
|
|
2
|
+
export { createCanonicalWavefrontMeshInputs } from "./canonical-wavefront-meshes.js";
|
|
2
3
|
export {
|
|
3
4
|
createAnimatedSceneRenderer,
|
|
4
5
|
} from "./animated-scene-renderer.js";
|
|
@@ -88,3 +89,5 @@ export {
|
|
|
88
89
|
defaultRendererClearColor,
|
|
89
90
|
supportsWebGpu,
|
|
90
91
|
} from "./renderer-webgpu-runtime.js";
|
|
92
|
+
|
|
93
|
+
export { validateNativeMaterials as validateNativeSurfaceMaterials } from './native-surface-materials.js';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createGpuRenderer } from "./renderer-webgpu-runtime.js";
|
|
2
2
|
import { nativeSceneShader } from "./native-scene-shader.js";
|
|
3
|
+
import {
|
|
4
|
+
validateNativeMaterials,
|
|
5
|
+
createNativeMaterialTextures,
|
|
6
|
+
} from "./native-surface-materials.js";
|
|
3
7
|
|
|
4
8
|
const STRIDE = 12;
|
|
5
9
|
const UNIFORM_BYTES = 256;
|
|
@@ -17,7 +21,7 @@ const unit = (a) => {
|
|
|
17
21
|
const finiteVector = (a) =>
|
|
18
22
|
Array.isArray(a) && a.length === 3 && a.every(Number.isFinite);
|
|
19
23
|
|
|
20
|
-
function validateFrame(frame, maxVertices) {
|
|
24
|
+
function validateFrame(frame, maxVertices, materialCount) {
|
|
21
25
|
const { vertices, camera, time = 0 } = frame ?? {};
|
|
22
26
|
if (
|
|
23
27
|
!(vertices instanceof Float32Array) ||
|
|
@@ -56,6 +60,48 @@ function validateFrame(frame, maxVertices) {
|
|
|
56
60
|
"wakes must contain at most four finite [x, z, heading] entries.",
|
|
57
61
|
);
|
|
58
62
|
}
|
|
63
|
+
const count = vertices.length / STRIDE;
|
|
64
|
+
if (
|
|
65
|
+
frame.texcoords !== undefined &&
|
|
66
|
+
(!(frame.texcoords instanceof Float32Array) ||
|
|
67
|
+
frame.texcoords.length !== count * 2 ||
|
|
68
|
+
!frame.texcoords.every(Number.isFinite))
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"texcoords must contain two finite coordinates per vertex.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (frame.surfaces !== undefined) {
|
|
75
|
+
if (
|
|
76
|
+
!Array.isArray(frame.surfaces) ||
|
|
77
|
+
frame.surfaces.length > 256 ||
|
|
78
|
+
!frame.texcoords
|
|
79
|
+
) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"surfaces require texcoords and at most 256 contiguous draw ranges.",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
let end = 0;
|
|
85
|
+
for (const range of frame.surfaces) {
|
|
86
|
+
if (
|
|
87
|
+
!range ||
|
|
88
|
+
range.firstVertex !== end ||
|
|
89
|
+
!Number.isInteger(range.vertexCount) ||
|
|
90
|
+
range.vertexCount <= 0 ||
|
|
91
|
+
range.vertexCount % 3 !== 0 ||
|
|
92
|
+
!Number.isInteger(range.materialIndex) ||
|
|
93
|
+
range.materialIndex < 0 ||
|
|
94
|
+
range.materialIndex >= materialCount
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"surface ranges must cover triangles in order and reference a valid material.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
end += range.vertexCount;
|
|
101
|
+
}
|
|
102
|
+
if (end !== count)
|
|
103
|
+
throw new Error("surface ranges must cover every vertex exactly once.");
|
|
104
|
+
}
|
|
59
105
|
return { ...frame, time };
|
|
60
106
|
}
|
|
61
107
|
|
|
@@ -106,6 +152,9 @@ function frameUniforms(frame, width, height, reflected = false) {
|
|
|
106
152
|
/** Native raster surfaces. The caller owns simulation, animation and scheduling. */
|
|
107
153
|
export async function createNativeSceneRenderer(options = {}) {
|
|
108
154
|
const maxVertices = options.maxVertices ?? 300_000;
|
|
155
|
+
const materials = options.materials ?? [];
|
|
156
|
+
validateNativeMaterials(materials);
|
|
157
|
+
const materialCount = materials.length;
|
|
109
158
|
if (
|
|
110
159
|
!Number.isInteger(maxVertices) ||
|
|
111
160
|
maxVertices < 3 ||
|
|
@@ -118,6 +167,7 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
118
167
|
let available = true;
|
|
119
168
|
let targets = null;
|
|
120
169
|
let vertexBuffer = null;
|
|
170
|
+
let uvBuffer = null;
|
|
121
171
|
let vertexCapacity = 0;
|
|
122
172
|
let pendingFrame = null;
|
|
123
173
|
let submittedFrames = 0;
|
|
@@ -186,6 +236,63 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
186
236
|
{ binding: 1, visibility: 2, sampler: { type: "filtering" } },
|
|
187
237
|
],
|
|
188
238
|
});
|
|
239
|
+
const materialLayout = device.createBindGroupLayout({
|
|
240
|
+
entries: [
|
|
241
|
+
...[0, 1, 2, 5].map((binding) => ({
|
|
242
|
+
binding,
|
|
243
|
+
visibility: 2,
|
|
244
|
+
texture: { sampleType: "float" },
|
|
245
|
+
})),
|
|
246
|
+
{ binding: 3, visibility: 2, sampler: { type: "filtering" } },
|
|
247
|
+
{
|
|
248
|
+
binding: 4,
|
|
249
|
+
visibility: 2,
|
|
250
|
+
buffer: { type: "uniform", minBindingSize: 16 },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
const materialSampler = device.createSampler({
|
|
255
|
+
addressModeU: "repeat",
|
|
256
|
+
addressModeV: "repeat",
|
|
257
|
+
minFilter: "linear",
|
|
258
|
+
magFilter: "linear",
|
|
259
|
+
mipmapFilter: "linear",
|
|
260
|
+
maxAnisotropy: 4,
|
|
261
|
+
});
|
|
262
|
+
const materialGroups = createNativeMaterialTextures(
|
|
263
|
+
device,
|
|
264
|
+
materials,
|
|
265
|
+
own,
|
|
266
|
+
).map((material) => {
|
|
267
|
+
const params = own(
|
|
268
|
+
device.createBuffer({
|
|
269
|
+
size: 16,
|
|
270
|
+
usage: 0x48,
|
|
271
|
+
label: "native.material-parameters",
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
device.queue.writeBuffer(
|
|
275
|
+
params,
|
|
276
|
+
0,
|
|
277
|
+
new Float32Array([
|
|
278
|
+
material.normalScale,
|
|
279
|
+
material.authored ? 1 : 0,
|
|
280
|
+
material.clearcoat,
|
|
281
|
+
material.clearcoatRoughness,
|
|
282
|
+
]),
|
|
283
|
+
);
|
|
284
|
+
return device.createBindGroup({
|
|
285
|
+
layout: materialLayout,
|
|
286
|
+
entries: [
|
|
287
|
+
{ binding: 0, resource: material.baseColor },
|
|
288
|
+
{ binding: 1, resource: material.normal },
|
|
289
|
+
{ binding: 2, resource: material.orm },
|
|
290
|
+
{ binding: 3, resource: materialSampler },
|
|
291
|
+
{ binding: 4, resource: { buffer: params } },
|
|
292
|
+
{ binding: 5, resource: material.clearcoatMap },
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
});
|
|
189
296
|
const uniformBuffer = own(
|
|
190
297
|
device.createBuffer({
|
|
191
298
|
label: "native.frame",
|
|
@@ -267,6 +374,20 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
267
374
|
? attributes.slice(0, 1)
|
|
268
375
|
: attributes,
|
|
269
376
|
},
|
|
377
|
+
...(vertex === "shadowVertex"
|
|
378
|
+
? []
|
|
379
|
+
: [
|
|
380
|
+
{
|
|
381
|
+
arrayStride: 8,
|
|
382
|
+
attributes: [
|
|
383
|
+
{
|
|
384
|
+
shaderLocation: 4,
|
|
385
|
+
offset: 0,
|
|
386
|
+
format: "float32x2",
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
]),
|
|
270
391
|
],
|
|
271
392
|
}
|
|
272
393
|
: {}),
|
|
@@ -303,7 +424,7 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
303
424
|
"surfaceFragment",
|
|
304
425
|
format,
|
|
305
426
|
4,
|
|
306
|
-
[uniformLayout, shadowLayout],
|
|
427
|
+
[uniformLayout, shadowLayout, materialLayout],
|
|
307
428
|
true,
|
|
308
429
|
);
|
|
309
430
|
const skyPipeline = await pipeline(
|
|
@@ -320,7 +441,7 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
320
441
|
"waterFragment",
|
|
321
442
|
format,
|
|
322
443
|
4,
|
|
323
|
-
[uniformLayout, shadowLayout, reflectionLayout],
|
|
444
|
+
[uniformLayout, shadowLayout, materialLayout, reflectionLayout],
|
|
324
445
|
);
|
|
325
446
|
const reflectedSurfacePipeline = await pipeline(
|
|
326
447
|
"reflection-surface",
|
|
@@ -328,7 +449,7 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
328
449
|
"reflectionSurface",
|
|
329
450
|
"rgba16float",
|
|
330
451
|
1,
|
|
331
|
-
[uniformLayout, shadowLayout],
|
|
452
|
+
[uniformLayout, shadowLayout, materialLayout],
|
|
332
453
|
true,
|
|
333
454
|
);
|
|
334
455
|
const reflectedSkyPipeline = await pipeline(
|
|
@@ -434,7 +555,20 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
434
555
|
if (p !== shadowPipeline) pass.setBindGroup(1, shadowGroup);
|
|
435
556
|
if (vertexCount > 0) {
|
|
436
557
|
pass.setVertexBuffer(0, vertexBuffer);
|
|
437
|
-
|
|
558
|
+
if (p === shadowPipeline) {
|
|
559
|
+
pass.draw(vertexCount);
|
|
560
|
+
} else {
|
|
561
|
+
pass.setVertexBuffer(1, uvBuffer);
|
|
562
|
+
if (pendingFrame.surfaces) {
|
|
563
|
+
for (const range of pendingFrame.surfaces) {
|
|
564
|
+
pass.setBindGroup(2, materialGroups[range.materialIndex + 1]);
|
|
565
|
+
pass.draw(range.vertexCount, 1, range.firstVertex);
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
pass.setBindGroup(2, materialGroups[0]);
|
|
569
|
+
pass.draw(vertexCount);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
438
572
|
}
|
|
439
573
|
};
|
|
440
574
|
const shadowPass = encoder.beginRenderPass({
|
|
@@ -480,7 +614,8 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
480
614
|
if (pendingFrame.water !== false) {
|
|
481
615
|
pass.setPipeline(waterPipeline);
|
|
482
616
|
pass.setBindGroup(1, shadowGroup);
|
|
483
|
-
pass.setBindGroup(2,
|
|
617
|
+
pass.setBindGroup(2, materialGroups[0]);
|
|
618
|
+
pass.setBindGroup(3, targets.reflectionGroup);
|
|
484
619
|
pass.draw(3);
|
|
485
620
|
}
|
|
486
621
|
drawGeometry(pass, surfacePipeline, mainGroup);
|
|
@@ -491,15 +626,23 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
491
626
|
render(input) {
|
|
492
627
|
if (destroyed) throw new Error("Native scene renderer was destroyed.");
|
|
493
628
|
if (!available) throw new Error("WebGPU device was lost.");
|
|
494
|
-
pendingFrame = validateFrame(input, maxVertices);
|
|
629
|
+
pendingFrame = validateFrame(input, maxVertices, materialCount);
|
|
495
630
|
const data = pendingFrame.vertices;
|
|
496
631
|
ensureTargets();
|
|
497
632
|
if (data.byteLength > vertexCapacity) {
|
|
498
633
|
if (vertexBuffer) release(vertexBuffer);
|
|
634
|
+
if (uvBuffer) release(uvBuffer);
|
|
499
635
|
vertexCapacity = Math.min(
|
|
500
636
|
maxVertices * STRIDE * 4,
|
|
501
637
|
Math.max(4096, 2 ** Math.ceil(Math.log2(data.byteLength))),
|
|
502
638
|
);
|
|
639
|
+
uvBuffer = own(
|
|
640
|
+
device.createBuffer({
|
|
641
|
+
label: "native.surface-uvs",
|
|
642
|
+
size: Math.ceil(vertexCapacity / (STRIDE * 4)) * 8,
|
|
643
|
+
usage: 0x28,
|
|
644
|
+
}),
|
|
645
|
+
);
|
|
503
646
|
vertexBuffer = own(
|
|
504
647
|
device.createBuffer({
|
|
505
648
|
label: "native.surface-vertices",
|
|
@@ -508,6 +651,8 @@ export async function createNativeSceneRenderer(options = {}) {
|
|
|
508
651
|
}),
|
|
509
652
|
);
|
|
510
653
|
}
|
|
654
|
+
if (pendingFrame.texcoords?.byteLength)
|
|
655
|
+
device.queue.writeBuffer(uvBuffer, 0, pendingFrame.texcoords);
|
|
511
656
|
vertexCount = data.length / STRIDE;
|
|
512
657
|
if (data.byteLength) device.queue.writeBuffer(vertexBuffer, 0, data);
|
|
513
658
|
device.queue.writeBuffer(
|