@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/CHANGELOG.md +16 -0
- package/README.md +35 -0
- package/dist/index.cjs +198 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/canonical-wavefront-meshes.js +226 -0
- package/src/index.d.ts +25 -0
- package/src/index.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,21 @@ All notable changes to this project will be documented in this file.
|
|
|
23
23
|
- **Security**
|
|
24
24
|
- (placeholder)
|
|
25
25
|
|
|
26
|
+
## [0.2.47] - 2026-10-09
|
|
27
|
+
|
|
28
|
+
- **Added**
|
|
29
|
+
- Added `createCanonicalWavefrontMeshInputs` to adapt decoded canonical model
|
|
30
|
+
geometry, material factors, and texture samples to Wavefront renderer inputs.
|
|
31
|
+
|
|
32
|
+
- **Changed**
|
|
33
|
+
- (placeholder)
|
|
34
|
+
|
|
35
|
+
- **Fixed**
|
|
36
|
+
- (placeholder)
|
|
37
|
+
|
|
38
|
+
- **Security**
|
|
39
|
+
- (placeholder)
|
|
40
|
+
|
|
26
41
|
## [0.2.46] - 2026-09-29
|
|
27
42
|
|
|
28
43
|
- Add native showcase surface drawing with filtered shadows, HDR planar water
|
|
@@ -1149,3 +1164,4 @@ All notable changes to this project will be documented in this file.
|
|
|
1149
1164
|
[0.2.44]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.44
|
|
1150
1165
|
[0.2.45]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.45
|
|
1151
1166
|
[0.2.46]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.46
|
|
1167
|
+
[0.2.47]: https://github.com/Plasius-LTD/gpu-renderer/releases/tag/v0.2.47
|
package/README.md
CHANGED
|
@@ -129,6 +129,41 @@ renderer.resize(window.innerWidth, window.innerHeight);
|
|
|
129
129
|
renderer.start();
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
+
## Canonical model ingestion
|
|
133
|
+
|
|
134
|
+
`createCanonicalWavefrontMeshInputs` adapts decoded primitive geometry and image
|
|
135
|
+
samples from the canonical model pipeline to the renderer's existing
|
|
136
|
+
`WavefrontMeshInput` shape. It maps canonical material factors and standard
|
|
137
|
+
texture bindings, preserves supported material extension data, and rejects
|
|
138
|
+
missing primitive, material, or decoded texture references. It accepts triangle
|
|
139
|
+
primitives; parsing, resource fetching, image decoding, and other topologies stay
|
|
140
|
+
in the upstream conversion layer. Metallic-roughness and unlit workflows are
|
|
141
|
+
supported; workflows without an existing Wavefront mapping fail explicitly.
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { createCanonicalWavefrontMeshInputs } from "@plasius/gpu-renderer";
|
|
145
|
+
|
|
146
|
+
const meshes = createCanonicalWavefrontMeshInputs({
|
|
147
|
+
document: canonicalDocument,
|
|
148
|
+
geometry: decodedPrimitives.map(({ primitiveId, positions, indices, normals, texcoords }) => ({
|
|
149
|
+
primitiveId,
|
|
150
|
+
positions,
|
|
151
|
+
indices,
|
|
152
|
+
normals,
|
|
153
|
+
texcoords,
|
|
154
|
+
})),
|
|
155
|
+
// ReadonlyMap<canonical texture id, decoded RGBA sample>
|
|
156
|
+
textures: decodedTextureSamples,
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The canonical document uses types from `@plasius/gpu-model-core`. Geometry is
|
|
161
|
+
already renderer-ready, normals may be omitted to retain the renderer's existing
|
|
162
|
+
fallback, and texture samples are keyed by canonical texture ID. The consuming
|
|
163
|
+
surface remains responsible for evaluating the remote
|
|
164
|
+
`gpu.model.conversion.enabled` flag: when disabled or rolled back, keep the
|
|
165
|
+
existing rendering path active.
|
|
166
|
+
|
|
132
167
|
## Adaptive Frame Hooks
|
|
133
168
|
|
|
134
169
|
`@plasius/gpu-renderer` now exposes frame lifecycle hooks so the app can pass
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
bucketFeedbackGameFrameTime: () => bucketFeedbackGameFrameTime,
|
|
29
29
|
bucketFeedbackGameViewport: () => bucketFeedbackGameViewport,
|
|
30
30
|
createAnimatedSceneRenderer: () => createAnimatedSceneRenderer,
|
|
31
|
+
createCanonicalWavefrontMeshInputs: () => createCanonicalWavefrontMeshInputs,
|
|
31
32
|
createDefaultWavefrontSceneObjects: () => createDefaultWavefrontSceneObjects,
|
|
32
33
|
createFeedbackGameDiagnosticSnapshot: () => createFeedbackGameDiagnosticSnapshot,
|
|
33
34
|
createGpuRenderer: () => createGpuRenderer,
|
|
@@ -1307,6 +1308,202 @@ async function createNativeSceneRenderer(options = {}) {
|
|
|
1307
1308
|
}
|
|
1308
1309
|
}
|
|
1309
1310
|
|
|
1311
|
+
// src/canonical-wavefront-meshes.js
|
|
1312
|
+
var STANDARD_TEXTURE_BINDINGS = Object.freeze([
|
|
1313
|
+
["baseColor", "baseColorTexture"],
|
|
1314
|
+
["metallicRoughness", "metallicRoughnessTexture"],
|
|
1315
|
+
["normal", "normalTexture"],
|
|
1316
|
+
["occlusion", "occlusionTexture"],
|
|
1317
|
+
["emissive", "emissiveTexture"]
|
|
1318
|
+
]);
|
|
1319
|
+
var EXTENSION_TEXTURE_BINDINGS = Object.freeze([
|
|
1320
|
+
["clearcoat", "clearcoat", "clearcoatTexture"],
|
|
1321
|
+
["clearcoatRoughness", "clearcoat", "clearcoatRoughnessTexture"],
|
|
1322
|
+
["clearcoatNormal", "clearcoat", "clearcoatNormalTexture"],
|
|
1323
|
+
["transmission", "transmission", "transmissionTexture"],
|
|
1324
|
+
["sheenColor", "sheen", "sheenColorTexture"],
|
|
1325
|
+
["sheenRoughness", "sheen", "sheenRoughnessTexture"]
|
|
1326
|
+
]);
|
|
1327
|
+
function requireArray(value, label) {
|
|
1328
|
+
if (!Array.isArray(value)) {
|
|
1329
|
+
throw new TypeError(`${label} must be an array.`);
|
|
1330
|
+
}
|
|
1331
|
+
return value;
|
|
1332
|
+
}
|
|
1333
|
+
function requireMap(value, label) {
|
|
1334
|
+
if (!value || typeof value !== "object" || typeof value.get !== "function") {
|
|
1335
|
+
throw new TypeError(`${label} must provide a ReadonlyMap-compatible get method.`);
|
|
1336
|
+
}
|
|
1337
|
+
return value;
|
|
1338
|
+
}
|
|
1339
|
+
function textureSample(binding, textures, label, extra = {}) {
|
|
1340
|
+
if (binding === void 0 || binding === null) {
|
|
1341
|
+
return void 0;
|
|
1342
|
+
}
|
|
1343
|
+
const sample = textures.get(binding.textureId);
|
|
1344
|
+
if (!sample) {
|
|
1345
|
+
throw new Error(`Missing decoded texture sample ${binding.textureId} for ${label}.`);
|
|
1346
|
+
}
|
|
1347
|
+
const width = Number(sample.width);
|
|
1348
|
+
const height = Number(sample.height);
|
|
1349
|
+
const data = sample.data;
|
|
1350
|
+
const dataLength = data && typeof data.length === "number" ? data.length : -1;
|
|
1351
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
1352
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} has invalid dimensions.`);
|
|
1353
|
+
}
|
|
1354
|
+
if (width > 4096 || height > 4096 || width * height > 4096 * 4096) {
|
|
1355
|
+
throw new RangeError(`Decoded texture sample ${binding.textureId} exceeds the 4096-pixel resource limit.`);
|
|
1356
|
+
}
|
|
1357
|
+
if (!(Array.isArray(data) || ArrayBuffer.isView(data)) || dataLength < width * height * 4) {
|
|
1358
|
+
throw new TypeError(`Decoded texture sample ${binding.textureId} must contain width \xD7 height \xD7 4 RGBA values.`);
|
|
1359
|
+
}
|
|
1360
|
+
return Object.freeze({
|
|
1361
|
+
...sample,
|
|
1362
|
+
...binding.texCoordSet === void 0 ? {} : { texCoord: binding.texCoordSet },
|
|
1363
|
+
...extra
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
function cloneMaterialExtensions(material, textures) {
|
|
1367
|
+
const extensions = { ...material.extensions ?? {} };
|
|
1368
|
+
if (material.workflow === "unlit") {
|
|
1369
|
+
const unlitKey = "KHR_materials_unlit";
|
|
1370
|
+
const unlitValue = extensions[unlitKey] ?? extensions.unlit ?? {};
|
|
1371
|
+
if (typeof unlitValue !== "object" || unlitValue === null || Array.isArray(unlitValue)) {
|
|
1372
|
+
throw new TypeError(`Canonical material extension ${unlitKey} must be an object.`);
|
|
1373
|
+
}
|
|
1374
|
+
extensions[unlitKey] = { ...unlitValue, enabled: true };
|
|
1375
|
+
}
|
|
1376
|
+
for (const [bindingName, extensionName, textureKey] of EXTENSION_TEXTURE_BINDINGS) {
|
|
1377
|
+
const binding = material.textures?.[bindingName];
|
|
1378
|
+
if (!binding) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const extensionKey = `KHR_materials_${extensionName}`;
|
|
1382
|
+
const extensionValue = extensions[extensionKey] ?? extensions[extensionName] ?? {};
|
|
1383
|
+
if (typeof extensionValue !== "object" || extensionValue === null || Array.isArray(extensionValue)) {
|
|
1384
|
+
throw new TypeError(`Canonical material extension ${extensionKey} must be an object.`);
|
|
1385
|
+
}
|
|
1386
|
+
extensions[extensionKey] = {
|
|
1387
|
+
...extensionValue,
|
|
1388
|
+
[textureKey]: textureSample(binding, textures, `${extensionKey}.${textureKey}`)
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
return extensions;
|
|
1392
|
+
}
|
|
1393
|
+
function mapMaterial(material, materialRefId, textures) {
|
|
1394
|
+
if (!material) {
|
|
1395
|
+
return { materialRefId };
|
|
1396
|
+
}
|
|
1397
|
+
if (material.alphaMode === "mask") {
|
|
1398
|
+
throw new Error("Canonical alphaMode mask is unsupported by WavefrontMeshInput.");
|
|
1399
|
+
}
|
|
1400
|
+
if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
|
|
1401
|
+
throw new Error(`Unsupported canonical material workflow: ${material.workflow}.`);
|
|
1402
|
+
}
|
|
1403
|
+
const mapped = {
|
|
1404
|
+
materialRefId,
|
|
1405
|
+
...material.alphaMode === "blend" ? { materialKind: "transparent" } : {},
|
|
1406
|
+
baseColor: material.baseColorFactor,
|
|
1407
|
+
emission: material.emissiveFactor ? [...material.emissiveFactor, 1] : [0, 0, 0, 1],
|
|
1408
|
+
roughness: material.roughnessFactor,
|
|
1409
|
+
metallic: material.metallicFactor,
|
|
1410
|
+
opacity: material.alphaMode === "opaque" ? 1 : material.baseColorFactor?.[3],
|
|
1411
|
+
clearcoat: material.clearcoatFactor,
|
|
1412
|
+
clearcoatRoughness: material.clearcoatRoughnessFactor,
|
|
1413
|
+
transmission: material.transmissionFactor,
|
|
1414
|
+
sheenColor: material.sheenColorFactor,
|
|
1415
|
+
extensions: cloneMaterialExtensions(material, textures)
|
|
1416
|
+
};
|
|
1417
|
+
for (const [bindingName, inputName] of STANDARD_TEXTURE_BINDINGS) {
|
|
1418
|
+
const binding = material.textures?.[bindingName];
|
|
1419
|
+
if (!binding) {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const extra = bindingName === "normal" && material.normalScale !== void 0 ? { scale: material.normalScale } : bindingName === "occlusion" && material.occlusionStrength !== void 0 ? { strength: material.occlusionStrength } : {};
|
|
1423
|
+
mapped[inputName] = textureSample(binding, textures, inputName, extra);
|
|
1424
|
+
}
|
|
1425
|
+
return mapped;
|
|
1426
|
+
}
|
|
1427
|
+
function createCanonicalWavefrontMeshInputs({ document, geometry, textures = /* @__PURE__ */ new Map() } = {}) {
|
|
1428
|
+
if (!document || typeof document !== "object") {
|
|
1429
|
+
throw new TypeError("Canonical model document is required.");
|
|
1430
|
+
}
|
|
1431
|
+
const meshes = requireArray(document.meshes, "Canonical model meshes");
|
|
1432
|
+
const materials = requireArray(document.materials, "Canonical model materials");
|
|
1433
|
+
const geometryInputs = requireArray(geometry, "Renderer-ready geometry");
|
|
1434
|
+
const textureSamples = requireMap(textures, "Decoded texture samples");
|
|
1435
|
+
const geometryByPrimitiveId = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const entry of geometryInputs) {
|
|
1437
|
+
if (!entry || typeof entry.primitiveId !== "string" || entry.primitiveId.length === 0) {
|
|
1438
|
+
throw new TypeError("Renderer-ready geometry requires a non-empty primitiveId.");
|
|
1439
|
+
}
|
|
1440
|
+
if (geometryByPrimitiveId.has(entry.primitiveId)) {
|
|
1441
|
+
throw new Error(`Duplicate renderer-ready geometry for primitive ${entry.primitiveId}.`);
|
|
1442
|
+
}
|
|
1443
|
+
geometryByPrimitiveId.set(entry.primitiveId, entry);
|
|
1444
|
+
}
|
|
1445
|
+
const materialsById = /* @__PURE__ */ new Map();
|
|
1446
|
+
const materialRefIds = /* @__PURE__ */ new Map();
|
|
1447
|
+
materials.forEach((material, index) => {
|
|
1448
|
+
if (!material || typeof material.id !== "string" || material.id.length === 0) {
|
|
1449
|
+
throw new TypeError("Canonical materials require a non-empty id.");
|
|
1450
|
+
}
|
|
1451
|
+
if (materialsById.has(material.id)) {
|
|
1452
|
+
throw new Error(`Duplicate canonical material ${material.id}.`);
|
|
1453
|
+
}
|
|
1454
|
+
materialsById.set(material.id, material);
|
|
1455
|
+
materialRefIds.set(material.id, index + 1);
|
|
1456
|
+
});
|
|
1457
|
+
const output = [];
|
|
1458
|
+
const consumedPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1459
|
+
const canonicalPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1460
|
+
for (const mesh of meshes) {
|
|
1461
|
+
if (!mesh || typeof mesh.id !== "string") {
|
|
1462
|
+
throw new TypeError("Canonical meshes require a string id.");
|
|
1463
|
+
}
|
|
1464
|
+
for (const primitive of requireArray(mesh.primitives, `Primitives for mesh ${mesh.id}`)) {
|
|
1465
|
+
if (!primitive || typeof primitive.id !== "string" || primitive.id.length === 0) {
|
|
1466
|
+
throw new TypeError(`Canonical mesh ${mesh.id} contains a primitive without an id.`);
|
|
1467
|
+
}
|
|
1468
|
+
if (canonicalPrimitiveIds.has(primitive.id)) {
|
|
1469
|
+
throw new Error(`Duplicate canonical primitive ${primitive.id}.`);
|
|
1470
|
+
}
|
|
1471
|
+
canonicalPrimitiveIds.add(primitive.id);
|
|
1472
|
+
if (primitive.topology !== "triangles") {
|
|
1473
|
+
throw new Error(`Unsupported canonical primitive topology: ${primitive.topology}.`);
|
|
1474
|
+
}
|
|
1475
|
+
const source = geometryByPrimitiveId.get(primitive.id);
|
|
1476
|
+
if (!source) {
|
|
1477
|
+
throw new Error(`Missing renderer-ready geometry for primitive ${primitive.id}.`);
|
|
1478
|
+
}
|
|
1479
|
+
consumedPrimitiveIds.add(primitive.id);
|
|
1480
|
+
let material = null;
|
|
1481
|
+
let materialRefId = 0;
|
|
1482
|
+
if (primitive.materialId !== void 0) {
|
|
1483
|
+
material = materialsById.get(primitive.materialId);
|
|
1484
|
+
if (!material) {
|
|
1485
|
+
throw new Error(`Missing canonical material ${primitive.materialId} for primitive ${primitive.id}.`);
|
|
1486
|
+
}
|
|
1487
|
+
materialRefId = materialRefIds.get(primitive.materialId);
|
|
1488
|
+
}
|
|
1489
|
+
output.push(Object.freeze({
|
|
1490
|
+
id: output.length + 1,
|
|
1491
|
+
positions: source.positions,
|
|
1492
|
+
...source.indices === void 0 ? {} : { indices: source.indices },
|
|
1493
|
+
...source.normals === void 0 ? {} : { normals: source.normals },
|
|
1494
|
+
...(source.texcoords ?? source.uvs) === void 0 ? {} : { uvs: source.texcoords ?? source.uvs },
|
|
1495
|
+
...mapMaterial(material, materialRefId, textureSamples)
|
|
1496
|
+
}));
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
for (const primitiveId of geometryByPrimitiveId.keys()) {
|
|
1500
|
+
if (!consumedPrimitiveIds.has(primitiveId)) {
|
|
1501
|
+
throw new Error(`Renderer-ready geometry references unknown primitive ${primitiveId}.`);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return Object.freeze(output);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1310
1507
|
// src/animated-scene-renderer.js
|
|
1311
1508
|
var import_gpu_camera = require("@plasius/gpu-camera");
|
|
1312
1509
|
|
|
@@ -14093,6 +14290,7 @@ function createRayTracingRenderPlan(options = {}) {
|
|
|
14093
14290
|
bucketFeedbackGameFrameTime,
|
|
14094
14291
|
bucketFeedbackGameViewport,
|
|
14095
14292
|
createAnimatedSceneRenderer,
|
|
14293
|
+
createCanonicalWavefrontMeshInputs,
|
|
14096
14294
|
createDefaultWavefrontSceneObjects,
|
|
14097
14295
|
createFeedbackGameDiagnosticSnapshot,
|
|
14098
14296
|
createGpuRenderer,
|