@plasius/gpu-renderer 0.2.46 → 0.2.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-renderer",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
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"
@@ -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;
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";