@plasius/gpu-shared 0.1.16 → 0.1.18
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 +9 -0
- package/README.md +6 -0
- package/dist/{chunk-6SOHFUOE.js → chunk-EZHA3MH7.js} +49 -17
- package/dist/chunk-EZHA3MH7.js.map +1 -0
- package/dist/chunk-UKCJ2AWJ.js +793 -0
- package/dist/chunk-UKCJ2AWJ.js.map +1 -0
- package/dist/{gltf-loader-B6VOWGBV.js → gltf-loader-FMRC3OEV.js} +2 -2
- package/dist/index.cjs +456 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -5
- package/dist/{product-studio-runtime-BYVBUWIN.js → product-studio-runtime-3KANG3JE.js} +3 -3
- package/dist/{showcase-runtime-M6TEUYOG.js → showcase-runtime-OH3H6ZW2.js} +2 -2
- package/package.json +1 -1
- package/src/gltf-loader.js +447 -15
- package/src/index.d.ts +83 -0
- package/src/product-studio-runtime.js +61 -26
- package/dist/chunk-6SOHFUOE.js.map +0 -1
- package/dist/chunk-QVNRTWHB.js +0 -445
- package/dist/chunk-QVNRTWHB.js.map +0 -1
- /package/dist/{gltf-loader-B6VOWGBV.js.map → gltf-loader-FMRC3OEV.js.map} +0 -0
- /package/dist/{product-studio-runtime-BYVBUWIN.js.map → product-studio-runtime-3KANG3JE.js.map} +0 -0
- /package/dist/{showcase-runtime-M6TEUYOG.js.map → showcase-runtime-OH3H6ZW2.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -295,6 +295,33 @@ function getTypeSize(type) {
|
|
|
295
295
|
throw new Error(`Unsupported glTF accessor type: ${type}`);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
function getComponentByteSize(componentType) {
|
|
299
|
+
switch (componentType) {
|
|
300
|
+
case 5121:
|
|
301
|
+
return 1;
|
|
302
|
+
case 5123:
|
|
303
|
+
return 2;
|
|
304
|
+
case 5125:
|
|
305
|
+
case 5126:
|
|
306
|
+
return 4;
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unsupported glTF componentType: ${componentType}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function readComponentValue(view, componentType, byteOffset) {
|
|
312
|
+
switch (componentType) {
|
|
313
|
+
case 5121:
|
|
314
|
+
return view.getUint8(byteOffset);
|
|
315
|
+
case 5123:
|
|
316
|
+
return view.getUint16(byteOffset, true);
|
|
317
|
+
case 5125:
|
|
318
|
+
return view.getUint32(byteOffset, true);
|
|
319
|
+
case 5126:
|
|
320
|
+
return view.getFloat32(byteOffset, true);
|
|
321
|
+
default:
|
|
322
|
+
throw new Error(`Unsupported glTF componentType: ${componentType}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
298
325
|
function readAccessor(document2, accessorIndex, buffers) {
|
|
299
326
|
const accessor = document2.accessors?.[accessorIndex];
|
|
300
327
|
if (!accessor) {
|
|
@@ -307,20 +334,255 @@ function readAccessor(document2, accessorIndex, buffers) {
|
|
|
307
334
|
const buffer = buffers[bufferView.buffer];
|
|
308
335
|
const componentCount = getTypeSize(accessor.type);
|
|
309
336
|
const byteOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
337
|
+
const componentByteSize = getComponentByteSize(accessor.componentType);
|
|
338
|
+
const packedElementByteLength = componentCount * componentByteSize;
|
|
339
|
+
const byteStride = Math.max(bufferView.byteStride ?? packedElementByteLength, packedElementByteLength);
|
|
340
|
+
let values;
|
|
341
|
+
if (byteStride === packedElementByteLength) {
|
|
342
|
+
const valueCount = accessor.count * componentCount;
|
|
343
|
+
values = Array.from(
|
|
344
|
+
getComponentArray(accessor.componentType, buffer, byteOffset, valueCount)
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
const view = new DataView(buffer, byteOffset);
|
|
348
|
+
values = new Array(accessor.count * componentCount);
|
|
349
|
+
for (let index = 0; index < accessor.count; index += 1) {
|
|
350
|
+
const elementOffset = index * byteStride;
|
|
351
|
+
for (let componentIndex = 0; componentIndex < componentCount; componentIndex += 1) {
|
|
352
|
+
values[index * componentCount + componentIndex] = readComponentValue(
|
|
353
|
+
view,
|
|
354
|
+
accessor.componentType,
|
|
355
|
+
elementOffset + componentIndex * componentByteSize
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
314
360
|
if (accessor.normalized) {
|
|
315
361
|
const scale = getNormalizationScale(accessor.componentType);
|
|
316
362
|
return values.map((value) => value / scale);
|
|
317
363
|
}
|
|
318
364
|
return values;
|
|
319
365
|
}
|
|
320
|
-
function
|
|
366
|
+
async function decodeImagePixels(blob, urlLabel = "glTF texture") {
|
|
367
|
+
if (typeof createImageBitmap === "function") {
|
|
368
|
+
const bitmap = await createImageBitmap(blob);
|
|
369
|
+
try {
|
|
370
|
+
const canvas = typeof OffscreenCanvas === "function" ? new OffscreenCanvas(bitmap.width, bitmap.height) : typeof document !== "undefined" ? Object.assign(document.createElement("canvas"), {
|
|
371
|
+
width: bitmap.width,
|
|
372
|
+
height: bitmap.height
|
|
373
|
+
}) : null;
|
|
374
|
+
const context = canvas?.getContext?.("2d", { willReadFrequently: true });
|
|
375
|
+
if (!context) {
|
|
376
|
+
throw new Error("Unable to create 2D context for glTF texture decode.");
|
|
377
|
+
}
|
|
378
|
+
context.drawImage(bitmap, 0, 0);
|
|
379
|
+
const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
380
|
+
return Object.freeze({
|
|
381
|
+
width: bitmap.width,
|
|
382
|
+
height: bitmap.height,
|
|
383
|
+
data: imageData.data
|
|
384
|
+
});
|
|
385
|
+
} finally {
|
|
386
|
+
bitmap.close?.();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (typeof document === "undefined") {
|
|
390
|
+
throw new Error(`Unable to decode ${urlLabel}: browser image decode APIs are unavailable.`);
|
|
391
|
+
}
|
|
392
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
393
|
+
try {
|
|
394
|
+
const image = await new Promise((resolve, reject) => {
|
|
395
|
+
const element = new Image();
|
|
396
|
+
element.onload = () => resolve(element);
|
|
397
|
+
element.onerror = () => reject(new Error(`Failed to decode ${urlLabel}.`));
|
|
398
|
+
element.src = objectUrl;
|
|
399
|
+
});
|
|
400
|
+
const canvas = Object.assign(document.createElement("canvas"), {
|
|
401
|
+
width: image.naturalWidth || image.width,
|
|
402
|
+
height: image.naturalHeight || image.height
|
|
403
|
+
});
|
|
404
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
405
|
+
if (!context) {
|
|
406
|
+
throw new Error("Unable to create 2D context for glTF texture decode.");
|
|
407
|
+
}
|
|
408
|
+
context.drawImage(image, 0, 0);
|
|
409
|
+
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
410
|
+
return Object.freeze({
|
|
411
|
+
width: canvas.width,
|
|
412
|
+
height: canvas.height,
|
|
413
|
+
data: imageData.data
|
|
414
|
+
});
|
|
415
|
+
} finally {
|
|
416
|
+
URL.revokeObjectURL(objectUrl);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function sliceBufferView(document2, bufferViewIndex, buffers) {
|
|
420
|
+
const bufferView = document2.bufferViews?.[bufferViewIndex];
|
|
421
|
+
if (!bufferView) {
|
|
422
|
+
throw new Error(`glTF bufferView ${bufferViewIndex} is missing.`);
|
|
423
|
+
}
|
|
424
|
+
const buffer = buffers[bufferView.buffer];
|
|
425
|
+
if (!buffer) {
|
|
426
|
+
throw new Error(`glTF buffer ${bufferView.buffer} is missing.`);
|
|
427
|
+
}
|
|
428
|
+
const start = bufferView.byteOffset ?? 0;
|
|
429
|
+
const end = start + (bufferView.byteLength ?? 0);
|
|
430
|
+
return buffer.slice(start, end);
|
|
431
|
+
}
|
|
432
|
+
async function loadImageResource(document2, image, index, buffers, baseUrl2) {
|
|
433
|
+
if (typeof image?.uri === "string") {
|
|
434
|
+
const response = await fetch(new URL(image.uri, baseUrl2));
|
|
435
|
+
if (!response.ok) {
|
|
436
|
+
throw new Error(`Failed to load glTF texture: ${response.status} ${response.statusText}`);
|
|
437
|
+
}
|
|
438
|
+
return decodeImagePixels(
|
|
439
|
+
await response.blob(),
|
|
440
|
+
`glTF texture ${index}${image.uri ? ` (${image.uri})` : ""}`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (typeof image?.bufferView === "number") {
|
|
444
|
+
const bytes = sliceBufferView(document2, image.bufferView, buffers);
|
|
445
|
+
return decodeImagePixels(
|
|
446
|
+
new Blob([bytes], { type: image.mimeType ?? "application/octet-stream" }),
|
|
447
|
+
`glTF texture ${index}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
function normalizeTextureTransformPair(value, fallback) {
|
|
453
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
454
|
+
return fallback;
|
|
455
|
+
}
|
|
456
|
+
return [
|
|
457
|
+
Number.isFinite(value[0]) ? Number(value[0]) : fallback[0],
|
|
458
|
+
Number.isFinite(value[1]) ? Number(value[1]) : fallback[1]
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
function readTextureTransform(textureRef) {
|
|
462
|
+
const transformExtension = textureRef?.extensions?.KHR_texture_transform ?? null;
|
|
463
|
+
return {
|
|
464
|
+
texCoord: typeof transformExtension?.texCoord === "number" ? transformExtension.texCoord : textureRef?.texCoord ?? 0,
|
|
465
|
+
offset: normalizeTextureTransformPair(transformExtension?.offset, [0, 0]),
|
|
466
|
+
scale: normalizeTextureTransformPair(transformExtension?.scale, [1, 1]),
|
|
467
|
+
rotation: Number.isFinite(transformExtension?.rotation) ? Number(transformExtension.rotation) : 0
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function wrapTextureCoordinate(value) {
|
|
471
|
+
return (value % 1 + 1) % 1;
|
|
472
|
+
}
|
|
473
|
+
function transformTextureCoordinate(uv, transform) {
|
|
474
|
+
const scaledU = uv[0] * transform.scale[0];
|
|
475
|
+
const scaledV = uv[1] * transform.scale[1];
|
|
476
|
+
const cosine = Math.cos(transform.rotation);
|
|
477
|
+
const sine = Math.sin(transform.rotation);
|
|
478
|
+
return [
|
|
479
|
+
scaledU * cosine - scaledV * sine + transform.offset[0],
|
|
480
|
+
scaledU * sine + scaledV * cosine + transform.offset[1]
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
function readTexturePixel(data, width, height, x, y) {
|
|
484
|
+
const clampedX = Math.min(width - 1, Math.max(0, x));
|
|
485
|
+
const clampedY = Math.min(height - 1, Math.max(0, y));
|
|
486
|
+
const offset = (clampedY * width + clampedX) * 4;
|
|
487
|
+
return [
|
|
488
|
+
data[offset] ?? 0,
|
|
489
|
+
data[offset + 1] ?? 0,
|
|
490
|
+
data[offset + 2] ?? 0,
|
|
491
|
+
data[offset + 3] ?? 255
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
function mixChannel(a, b, weight) {
|
|
495
|
+
return a + (b - a) * weight;
|
|
496
|
+
}
|
|
497
|
+
function sampleTexturePixel(data, width, height, uv) {
|
|
498
|
+
const u = wrapTextureCoordinate(uv[0]);
|
|
499
|
+
const v = wrapTextureCoordinate(uv[1]);
|
|
500
|
+
const sourceX = u * Math.max(width - 1, 0);
|
|
501
|
+
const sourceY = (1 - v) * Math.max(height - 1, 0);
|
|
502
|
+
const x0 = Math.floor(sourceX);
|
|
503
|
+
const y0 = Math.floor(sourceY);
|
|
504
|
+
const x1 = Math.min(width - 1, x0 + 1);
|
|
505
|
+
const y1 = Math.min(height - 1, y0 + 1);
|
|
506
|
+
const tx = sourceX - x0;
|
|
507
|
+
const ty = sourceY - y0;
|
|
508
|
+
const topLeft = readTexturePixel(data, width, height, x0, y0);
|
|
509
|
+
const topRight = readTexturePixel(data, width, height, x1, y0);
|
|
510
|
+
const bottomLeft = readTexturePixel(data, width, height, x0, y1);
|
|
511
|
+
const bottomRight = readTexturePixel(data, width, height, x1, y1);
|
|
512
|
+
return [0, 1, 2, 3].map((channelIndex) => {
|
|
513
|
+
const top = mixChannel(topLeft[channelIndex], topRight[channelIndex], tx);
|
|
514
|
+
const bottom = mixChannel(bottomLeft[channelIndex], bottomRight[channelIndex], tx);
|
|
515
|
+
return mixChannel(top, bottom, ty);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
function applyTextureTransformToPixels(pixels, transform) {
|
|
519
|
+
const isIdentityTransform = transform.offset[0] === 0 && transform.offset[1] === 0 && transform.scale[0] === 1 && transform.scale[1] === 1 && transform.rotation === 0;
|
|
520
|
+
if (isIdentityTransform) {
|
|
521
|
+
return pixels;
|
|
522
|
+
}
|
|
523
|
+
const transformedData = new Uint8ClampedArray(pixels.data.length);
|
|
524
|
+
for (let y = 0; y < pixels.height; y += 1) {
|
|
525
|
+
const outputV = pixels.height > 1 ? 1 - y / (pixels.height - 1) : 0;
|
|
526
|
+
for (let x = 0; x < pixels.width; x += 1) {
|
|
527
|
+
const outputU = pixels.width > 1 ? x / (pixels.width - 1) : 0;
|
|
528
|
+
const sourcePixel = sampleTexturePixel(
|
|
529
|
+
pixels.data,
|
|
530
|
+
pixels.width,
|
|
531
|
+
pixels.height,
|
|
532
|
+
transformTextureCoordinate([outputU, outputV], transform)
|
|
533
|
+
);
|
|
534
|
+
const offset = (y * pixels.width + x) * 4;
|
|
535
|
+
transformedData[offset] = sourcePixel[0];
|
|
536
|
+
transformedData[offset + 1] = sourcePixel[1];
|
|
537
|
+
transformedData[offset + 2] = sourcePixel[2];
|
|
538
|
+
transformedData[offset + 3] = sourcePixel[3];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
width: pixels.width,
|
|
543
|
+
height: pixels.height,
|
|
544
|
+
data: transformedData
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function getMaterialTexture(document2, textureRef, imageResources) {
|
|
548
|
+
if (!textureRef || typeof textureRef.index !== "number") {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const texture = document2.textures?.[textureRef.index] ?? null;
|
|
552
|
+
const sourceIndex = texture?.source;
|
|
553
|
+
if (typeof sourceIndex !== "number") {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const pixels = imageResources.get(sourceIndex) ?? null;
|
|
557
|
+
if (!pixels) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
const transform = readTextureTransform(textureRef);
|
|
561
|
+
const transformedPixels = applyTextureTransformToPixels(pixels, transform);
|
|
562
|
+
return Object.freeze({
|
|
563
|
+
texCoord: transform.texCoord,
|
|
564
|
+
scale: textureRef.scale,
|
|
565
|
+
strength: textureRef.strength,
|
|
566
|
+
width: transformedPixels.width,
|
|
567
|
+
height: transformedPixels.height,
|
|
568
|
+
data: transformedPixels.data
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function getMaterialInfo(document2, primitive, imageResources) {
|
|
321
572
|
const material = document2.materials?.[primitive.material] ?? null;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
573
|
+
const pbr = material?.pbrMetallicRoughness ?? null;
|
|
574
|
+
const factor = pbr?.baseColorFactor ?? [0.56, 0.33, 0.22, 1];
|
|
575
|
+
const emissive = Array.isArray(material?.emissiveFactor) ? material.emissiveFactor : [0, 0, 0];
|
|
576
|
+
const extensions = material?.extensions ?? {};
|
|
577
|
+
const specular = extensions.KHR_materials_specular ?? null;
|
|
578
|
+
const transmission = extensions.KHR_materials_transmission ?? null;
|
|
579
|
+
const ior = extensions.KHR_materials_ior ?? null;
|
|
580
|
+
const clearcoat = extensions.KHR_materials_clearcoat ?? null;
|
|
581
|
+
const sheen = extensions.KHR_materials_sheen ?? null;
|
|
582
|
+
const volume = extensions.KHR_materials_volume ?? null;
|
|
583
|
+
const iridescence = extensions.KHR_materials_iridescence ?? null;
|
|
584
|
+
const anisotropy = extensions.KHR_materials_anisotropy ?? null;
|
|
585
|
+
const dispersion = extensions.KHR_materials_dispersion ?? null;
|
|
324
586
|
return Object.freeze({
|
|
325
587
|
name: material?.name ?? "default-material",
|
|
326
588
|
color: Object.freeze({
|
|
@@ -329,13 +591,88 @@ function getMaterialInfo(document2, primitive) {
|
|
|
329
591
|
b: factor[2],
|
|
330
592
|
a: factor[3] ?? 1
|
|
331
593
|
}),
|
|
332
|
-
roughness: typeof
|
|
333
|
-
metallic: typeof
|
|
594
|
+
roughness: typeof pbr?.roughnessFactor === "number" ? pbr.roughnessFactor : 0.92,
|
|
595
|
+
metallic: typeof pbr?.metallicFactor === "number" ? pbr.metallicFactor : 0.08,
|
|
596
|
+
opacity: factor[3] ?? 1,
|
|
334
597
|
emissive: Object.freeze({
|
|
335
598
|
r: emissive[0] ?? 0,
|
|
336
599
|
g: emissive[1] ?? 0,
|
|
337
|
-
b: emissive[2] ?? 0
|
|
338
|
-
|
|
600
|
+
b: emissive[2] ?? 0,
|
|
601
|
+
a: 1
|
|
602
|
+
}),
|
|
603
|
+
baseColorTexture: getMaterialTexture(document2, pbr?.baseColorTexture, imageResources),
|
|
604
|
+
metallicRoughnessTexture: getMaterialTexture(
|
|
605
|
+
document2,
|
|
606
|
+
pbr?.metallicRoughnessTexture,
|
|
607
|
+
imageResources
|
|
608
|
+
),
|
|
609
|
+
normalTexture: getMaterialTexture(document2, material?.normalTexture, imageResources),
|
|
610
|
+
occlusionTexture: getMaterialTexture(document2, material?.occlusionTexture, imageResources),
|
|
611
|
+
emissiveTexture: getMaterialTexture(document2, material?.emissiveTexture, imageResources),
|
|
612
|
+
specular: typeof specular?.specularFactor === "number" ? specular.specularFactor : 1,
|
|
613
|
+
specularColor: Object.freeze(
|
|
614
|
+
Array.isArray(specular?.specularColorFactor) ? [...specular.specularColorFactor] : [1, 1, 1]
|
|
615
|
+
),
|
|
616
|
+
specularTexture: getMaterialTexture(document2, specular?.specularTexture, imageResources),
|
|
617
|
+
specularColorTexture: getMaterialTexture(
|
|
618
|
+
document2,
|
|
619
|
+
specular?.specularColorTexture,
|
|
620
|
+
imageResources
|
|
621
|
+
),
|
|
622
|
+
transmission: typeof transmission?.transmissionFactor === "number" ? transmission.transmissionFactor : 0,
|
|
623
|
+
transmissionTexture: getMaterialTexture(
|
|
624
|
+
document2,
|
|
625
|
+
transmission?.transmissionTexture,
|
|
626
|
+
imageResources
|
|
627
|
+
),
|
|
628
|
+
ior: typeof ior?.ior === "number" ? ior.ior : 1.45,
|
|
629
|
+
attenuationDistance: typeof volume?.attenuationDistance === "number" ? volume.attenuationDistance : null,
|
|
630
|
+
attenuationColor: Object.freeze(
|
|
631
|
+
Array.isArray(volume?.attenuationColor) ? [...volume.attenuationColor] : [1, 1, 1]
|
|
632
|
+
),
|
|
633
|
+
thickness: typeof volume?.thicknessFactor === "number" ? volume.thicknessFactor : 0,
|
|
634
|
+
thicknessTexture: getMaterialTexture(document2, volume?.thicknessTexture, imageResources),
|
|
635
|
+
clearcoat: typeof clearcoat?.clearcoatFactor === "number" ? clearcoat.clearcoatFactor : 0,
|
|
636
|
+
clearcoatTexture: getMaterialTexture(document2, clearcoat?.clearcoatTexture, imageResources),
|
|
637
|
+
clearcoatRoughness: typeof clearcoat?.clearcoatRoughnessFactor === "number" ? clearcoat.clearcoatRoughnessFactor : 0.08,
|
|
638
|
+
clearcoatRoughnessTexture: getMaterialTexture(
|
|
639
|
+
document2,
|
|
640
|
+
clearcoat?.clearcoatRoughnessTexture,
|
|
641
|
+
imageResources
|
|
642
|
+
),
|
|
643
|
+
clearcoatNormalTexture: getMaterialTexture(
|
|
644
|
+
document2,
|
|
645
|
+
clearcoat?.clearcoatNormalTexture,
|
|
646
|
+
imageResources
|
|
647
|
+
),
|
|
648
|
+
sheenColor: Object.freeze(
|
|
649
|
+
Array.isArray(sheen?.sheenColorFactor) ? [...sheen.sheenColorFactor] : [0, 0, 0]
|
|
650
|
+
),
|
|
651
|
+
sheenColorTexture: getMaterialTexture(document2, sheen?.sheenColorTexture, imageResources),
|
|
652
|
+
sheenRoughness: typeof sheen?.sheenRoughnessFactor === "number" ? sheen.sheenRoughnessFactor : 0,
|
|
653
|
+
sheenRoughnessTexture: getMaterialTexture(
|
|
654
|
+
document2,
|
|
655
|
+
sheen?.sheenRoughnessTexture,
|
|
656
|
+
imageResources
|
|
657
|
+
),
|
|
658
|
+
iridescence: typeof iridescence?.iridescenceFactor === "number" ? iridescence.iridescenceFactor : 0,
|
|
659
|
+
iridescenceTexture: getMaterialTexture(
|
|
660
|
+
document2,
|
|
661
|
+
iridescence?.iridescenceTexture,
|
|
662
|
+
imageResources
|
|
663
|
+
),
|
|
664
|
+
iridescenceIor: typeof iridescence?.iridescenceIor === "number" ? iridescence.iridescenceIor : 1.3,
|
|
665
|
+
iridescenceThicknessMinimum: typeof iridescence?.iridescenceThicknessMinimum === "number" ? iridescence.iridescenceThicknessMinimum : 100,
|
|
666
|
+
iridescenceThicknessMaximum: typeof iridescence?.iridescenceThicknessMaximum === "number" ? iridescence.iridescenceThicknessMaximum : 400,
|
|
667
|
+
iridescenceThicknessTexture: getMaterialTexture(
|
|
668
|
+
document2,
|
|
669
|
+
iridescence?.iridescenceThicknessTexture,
|
|
670
|
+
imageResources
|
|
671
|
+
),
|
|
672
|
+
anisotropy: typeof anisotropy?.anisotropyStrength === "number" ? anisotropy.anisotropyStrength : 0,
|
|
673
|
+
anisotropyRotation: typeof anisotropy?.anisotropyRotation === "number" ? anisotropy.anisotropyRotation : 0,
|
|
674
|
+
anisotropyTexture: getMaterialTexture(document2, anisotropy?.anisotropyTexture, imageResources),
|
|
675
|
+
dispersion: typeof dispersion?.dispersion === "number" ? dispersion.dispersion : 0
|
|
339
676
|
});
|
|
340
677
|
}
|
|
341
678
|
function computeBounds(positions) {
|
|
@@ -467,7 +804,7 @@ function transformNormal(normal, matrix) {
|
|
|
467
804
|
const length = Math.hypot(transformed[0], transformed[1], transformed[2]) || 1;
|
|
468
805
|
return [transformed[0] / length, transformed[1] / length, transformed[2] / length];
|
|
469
806
|
}
|
|
470
|
-
function collectScenePrimitives(document2, buffers) {
|
|
807
|
+
function collectScenePrimitives(document2, buffers, imageResources) {
|
|
471
808
|
const scene = document2.scenes?.[document2.scene ?? 0];
|
|
472
809
|
if (!scene || !Array.isArray(scene.nodes) || scene.nodes.length === 0) {
|
|
473
810
|
throw new Error("glTF demo asset must expose a default scene with at least one node.");
|
|
@@ -497,6 +834,7 @@ function collectScenePrimitives(document2, buffers) {
|
|
|
497
834
|
const positions = readAccessor(document2, primitive.attributes.POSITION, buffers);
|
|
498
835
|
const normals = typeof primitive.attributes.NORMAL === "number" ? readAccessor(document2, primitive.attributes.NORMAL, buffers) : null;
|
|
499
836
|
const colors = typeof primitive.attributes.COLOR_0 === "number" ? readAccessor(document2, primitive.attributes.COLOR_0, buffers) : null;
|
|
837
|
+
const uvs = typeof primitive.attributes.TEXCOORD_0 === "number" ? readAccessor(document2, primitive.attributes.TEXCOORD_0, buffers) : null;
|
|
500
838
|
const transformedPositions = [];
|
|
501
839
|
const transformedNormals = [];
|
|
502
840
|
for (let index = 0; index < positions.length; index += 3) {
|
|
@@ -514,7 +852,7 @@ function collectScenePrimitives(document2, buffers) {
|
|
|
514
852
|
}
|
|
515
853
|
}
|
|
516
854
|
const indices = typeof primitive.indices === "number" ? readAccessor(document2, primitive.indices, buffers).map((value) => Number(value)) : Array.from({ length: transformedPositions.length / 3 }, (_, index) => index);
|
|
517
|
-
const material = getMaterialInfo(document2, primitive);
|
|
855
|
+
const material = getMaterialInfo(document2, primitive, imageResources);
|
|
518
856
|
const primitiveName = `${node.name ?? mesh.name ?? "mesh"}-${primitiveIndex}`;
|
|
519
857
|
results.push(
|
|
520
858
|
Object.freeze({
|
|
@@ -522,6 +860,7 @@ function collectScenePrimitives(document2, buffers) {
|
|
|
522
860
|
positions: Object.freeze(transformedPositions),
|
|
523
861
|
indices: Object.freeze(indices),
|
|
524
862
|
normals: transformedNormals.length > 0 ? Object.freeze(transformedNormals) : null,
|
|
863
|
+
uvs: uvs ? Object.freeze(uvs) : null,
|
|
525
864
|
colors: colors ? Object.freeze(colors) : null,
|
|
526
865
|
material,
|
|
527
866
|
bounds: computeBounds(transformedPositions)
|
|
@@ -577,7 +916,16 @@ async function buildGltfModel(document2, baseUrl2) {
|
|
|
577
916
|
return nested.arrayBuffer();
|
|
578
917
|
})
|
|
579
918
|
);
|
|
580
|
-
const
|
|
919
|
+
const imageResources = /* @__PURE__ */ new Map();
|
|
920
|
+
await Promise.all(
|
|
921
|
+
(document2.images ?? []).map(async (image, index) => {
|
|
922
|
+
const pixels = await loadImageResource(document2, image, index, buffers, baseUrl2);
|
|
923
|
+
if (pixels) {
|
|
924
|
+
imageResources.set(index, pixels);
|
|
925
|
+
}
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
const scene = collectScenePrimitives(document2, buffers, imageResources);
|
|
581
929
|
const aggregatePositions = [];
|
|
582
930
|
const aggregateIndices = [];
|
|
583
931
|
for (const primitive of scene.primitives) {
|
|
@@ -2062,9 +2410,6 @@ __export(product_studio_runtime_exports, {
|
|
|
2062
2410
|
createProductStudioMeshes: () => createProductStudioMeshes,
|
|
2063
2411
|
mountGpuProductStudio: () => mountGpuProductStudio
|
|
2064
2412
|
});
|
|
2065
|
-
function clamp(value, min, max) {
|
|
2066
|
-
return Math.max(min, Math.min(max, value));
|
|
2067
|
-
}
|
|
2068
2413
|
function isFiniteVector(value) {
|
|
2069
2414
|
return Array.isArray(value) && value.length >= 3 && Number.isFinite(value[0]) && Number.isFinite(value[1]) && Number.isFinite(value[2]);
|
|
2070
2415
|
}
|
|
@@ -2253,18 +2598,56 @@ function createProductStudioMeshFromPrimitive(primitive, primitiveIndex, transfo
|
|
|
2253
2598
|
const indices = Array.isArray(primitive.indices) && primitive.indices.length >= 3 ? [...primitive.indices] : Array.from({ length: positions.length / 3 }, (_, index) => index);
|
|
2254
2599
|
const material = primitive.material ?? {};
|
|
2255
2600
|
const color = readMaterialColor(material);
|
|
2601
|
+
const uvs = Array.isArray(primitive.uvs) ? [...primitive.uvs] : null;
|
|
2256
2602
|
return Object.freeze({
|
|
2257
2603
|
id: 1e3 + primitiveIndex,
|
|
2258
2604
|
positions: Object.freeze(positions),
|
|
2259
2605
|
indices: Object.freeze(indices),
|
|
2260
2606
|
normals: Array.isArray(primitive.normals) ? Object.freeze([...primitive.normals]) : null,
|
|
2607
|
+
uvs: uvs ? Object.freeze(uvs) : null,
|
|
2608
|
+
material: Object.freeze({ ...material }),
|
|
2261
2609
|
color: Object.freeze(color),
|
|
2262
2610
|
emission: Object.freeze(readEmission(material)),
|
|
2263
2611
|
materialKind: readMaterialKind(material),
|
|
2264
2612
|
materialRefId: 1e3 + primitiveIndex,
|
|
2265
2613
|
roughness: Number.isFinite(material.roughness) ? material.roughness : 0.72,
|
|
2266
2614
|
metallic: Number.isFinite(material.metallic) ? material.metallic : 0,
|
|
2267
|
-
opacity: color[3]
|
|
2615
|
+
opacity: color[3],
|
|
2616
|
+
baseColorTexture: material.baseColorTexture ?? null,
|
|
2617
|
+
metallicRoughnessTexture: material.metallicRoughnessTexture ?? null,
|
|
2618
|
+
normalTexture: material.normalTexture ?? null,
|
|
2619
|
+
occlusionTexture: material.occlusionTexture ?? null,
|
|
2620
|
+
emissiveTexture: material.emissiveTexture ?? null,
|
|
2621
|
+
specular: Number.isFinite(material.specular) ? material.specular : 1,
|
|
2622
|
+
specularColor: Array.isArray(material.specularColor) ? Object.freeze([...material.specularColor]) : void 0,
|
|
2623
|
+
specularTexture: material.specularTexture ?? null,
|
|
2624
|
+
specularColorTexture: material.specularColorTexture ?? null,
|
|
2625
|
+
transmission: Number.isFinite(material.transmission) ? material.transmission : 0,
|
|
2626
|
+
transmissionTexture: material.transmissionTexture ?? null,
|
|
2627
|
+
ior: Number.isFinite(material.ior) ? material.ior : void 0,
|
|
2628
|
+
thickness: Number.isFinite(material.thickness) ? material.thickness : void 0,
|
|
2629
|
+
thicknessTexture: material.thicknessTexture ?? null,
|
|
2630
|
+
attenuationDistance: Number.isFinite(material.attenuationDistance) ? material.attenuationDistance : null,
|
|
2631
|
+
attenuationColor: Array.isArray(material.attenuationColor) ? Object.freeze([...material.attenuationColor]) : void 0,
|
|
2632
|
+
clearcoat: Number.isFinite(material.clearcoat) ? material.clearcoat : void 0,
|
|
2633
|
+
clearcoatTexture: material.clearcoatTexture ?? null,
|
|
2634
|
+
clearcoatRoughness: Number.isFinite(material.clearcoatRoughness) ? material.clearcoatRoughness : void 0,
|
|
2635
|
+
clearcoatRoughnessTexture: material.clearcoatRoughnessTexture ?? null,
|
|
2636
|
+
clearcoatNormalTexture: material.clearcoatNormalTexture ?? null,
|
|
2637
|
+
sheenColor: Array.isArray(material.sheenColor) ? Object.freeze([...material.sheenColor]) : void 0,
|
|
2638
|
+
sheenColorTexture: material.sheenColorTexture ?? null,
|
|
2639
|
+
sheenRoughness: Number.isFinite(material.sheenRoughness) ? material.sheenRoughness : void 0,
|
|
2640
|
+
sheenRoughnessTexture: material.sheenRoughnessTexture ?? null,
|
|
2641
|
+
iridescence: Number.isFinite(material.iridescence) ? material.iridescence : void 0,
|
|
2642
|
+
iridescenceTexture: material.iridescenceTexture ?? null,
|
|
2643
|
+
iridescenceIor: Number.isFinite(material.iridescenceIor) ? material.iridescenceIor : void 0,
|
|
2644
|
+
iridescenceThicknessMinimum: Number.isFinite(material.iridescenceThicknessMinimum) ? material.iridescenceThicknessMinimum : void 0,
|
|
2645
|
+
iridescenceThicknessMaximum: Number.isFinite(material.iridescenceThicknessMaximum) ? material.iridescenceThicknessMaximum : void 0,
|
|
2646
|
+
iridescenceThicknessTexture: material.iridescenceThicknessTexture ?? null,
|
|
2647
|
+
anisotropy: Number.isFinite(material.anisotropy) ? material.anisotropy : void 0,
|
|
2648
|
+
anisotropyRotation: Number.isFinite(material.anisotropyRotation) ? material.anisotropyRotation : void 0,
|
|
2649
|
+
anisotropyTexture: material.anisotropyTexture ?? null,
|
|
2650
|
+
dispersion: Number.isFinite(material.dispersion) ? material.dispersion : void 0
|
|
2268
2651
|
});
|
|
2269
2652
|
}
|
|
2270
2653
|
function createProductStudioMeshes(model, options = {}) {
|
|
@@ -2322,16 +2705,10 @@ function resolveRoot(options) {
|
|
|
2322
2705
|
}
|
|
2323
2706
|
return root;
|
|
2324
2707
|
}
|
|
2325
|
-
function resolveRenderSize(
|
|
2326
|
-
const rect = root.getBoundingClientRect?.() ?? { width: 1280, height: 720 };
|
|
2327
|
-
const devicePixelRatio = Number.isFinite(options.devicePixelRatio) ? options.devicePixelRatio : Number.isFinite(globalThis.window?.devicePixelRatio) ? globalThis.window.devicePixelRatio : 1;
|
|
2328
|
-
const cssWidth = Number.isFinite(rect.width) && rect.width > 0 ? rect.width : 1280;
|
|
2329
|
-
const cssHeight = Number.isFinite(rect.height) && rect.height > 0 ? rect.height : cssWidth * (9 / 16);
|
|
2330
|
-
const width = Number.isFinite(options.width) ? Math.trunc(options.width) : clamp(Math.round(cssWidth * devicePixelRatio), 640, 1920);
|
|
2331
|
-
const height = Number.isFinite(options.height) ? Math.trunc(options.height) : clamp(Math.round(cssHeight * devicePixelRatio), 360, 1080);
|
|
2708
|
+
function resolveRenderSize(options) {
|
|
2332
2709
|
return {
|
|
2333
|
-
width,
|
|
2334
|
-
height
|
|
2710
|
+
width: Number.isFinite(options.width) ? Math.trunc(options.width) : DEFAULT_RENDER_WIDTH,
|
|
2711
|
+
height: Number.isFinite(options.height) ? Math.trunc(options.height) : DEFAULT_RENDER_HEIGHT
|
|
2335
2712
|
};
|
|
2336
2713
|
}
|
|
2337
2714
|
function installSnapshotHook(state) {
|
|
@@ -2402,13 +2779,13 @@ async function mountGpuProductStudio(options = {}, featureFlags = null) {
|
|
|
2402
2779
|
if (typeof rendererModule.createWavefrontPathTracingComputeRenderer !== "function") {
|
|
2403
2780
|
throw new Error("Product Studio renderer loader must provide createWavefrontPathTracingComputeRenderer.");
|
|
2404
2781
|
}
|
|
2405
|
-
const size = resolveRenderSize(
|
|
2782
|
+
const size = resolveRenderSize(options);
|
|
2406
2783
|
const lightingOptions = await resolveWavefrontLightingOptions(options);
|
|
2407
2784
|
const renderer = await rendererModule.createWavefrontPathTracingComputeRenderer({
|
|
2408
2785
|
canvas,
|
|
2409
2786
|
width: size.width,
|
|
2410
2787
|
height: size.height,
|
|
2411
|
-
maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth :
|
|
2788
|
+
maxDepth: Number.isFinite(options.maxDepth) ? options.maxDepth : DEFAULT_RENDER_MAX_DEPTH,
|
|
2412
2789
|
tileSize: Number.isFinite(options.tileSize) ? options.tileSize : 128,
|
|
2413
2790
|
samplesPerPixel: Number.isFinite(options.samplesPerPixel) ? options.samplesPerPixel : 8,
|
|
2414
2791
|
denoise: options.denoise !== false,
|
|
@@ -2450,7 +2827,7 @@ async function mountGpuProductStudio(options = {}, featureFlags = null) {
|
|
|
2450
2827
|
}
|
|
2451
2828
|
});
|
|
2452
2829
|
}
|
|
2453
|
-
var STYLE_ID, DEFAULT_PRODUCT_ASSET_URL, DEFAULT_TARGET_CENTER, DEFAULT_TARGET_SIZE;
|
|
2830
|
+
var STYLE_ID, DEFAULT_PRODUCT_ASSET_URL, DEFAULT_TARGET_CENTER, DEFAULT_TARGET_SIZE, DEFAULT_RENDER_WIDTH, DEFAULT_RENDER_HEIGHT, DEFAULT_RENDER_MAX_DEPTH;
|
|
2454
2831
|
var init_product_studio_runtime = __esm({
|
|
2455
2832
|
"src/product-studio-runtime.js"() {
|
|
2456
2833
|
init_gltf_loader();
|
|
@@ -2458,6 +2835,9 @@ var init_product_studio_runtime = __esm({
|
|
|
2458
2835
|
DEFAULT_PRODUCT_ASSET_URL = "/data/models/eames-lounge-chair-ottoman/Eames_Lounge_Chair_Ottoman.gltf";
|
|
2459
2836
|
DEFAULT_TARGET_CENTER = Object.freeze([0, 0.74, 0]);
|
|
2460
2837
|
DEFAULT_TARGET_SIZE = 2.25;
|
|
2838
|
+
DEFAULT_RENDER_WIDTH = 640;
|
|
2839
|
+
DEFAULT_RENDER_HEIGHT = 360;
|
|
2840
|
+
DEFAULT_RENDER_MAX_DEPTH = 2;
|
|
2461
2841
|
}
|
|
2462
2842
|
});
|
|
2463
2843
|
|
|
@@ -2578,17 +2958,17 @@ function createFallbackPerformanceFeatureModule() {
|
|
|
2578
2958
|
let pressureLevel = "stable";
|
|
2579
2959
|
let frameSamples = 0;
|
|
2580
2960
|
let averageMs = 16.67;
|
|
2581
|
-
const
|
|
2961
|
+
const clamp2 = (next = 16.67) => Number.isFinite(next) ? Math.max(1, next) : 16.67;
|
|
2582
2962
|
const target = Object.freeze({
|
|
2583
2963
|
targetFrameTimeMs: 16.67,
|
|
2584
|
-
downgradeFrameTimeMs:
|
|
2585
|
-
upgradeFrameTimeMs:
|
|
2964
|
+
downgradeFrameTimeMs: clamp2(adaptation?.degradeThresholdMs ?? 20),
|
|
2965
|
+
upgradeFrameTimeMs: clamp2(adaptation?.upgradeThresholdMs ?? 14)
|
|
2586
2966
|
});
|
|
2587
2967
|
return {
|
|
2588
2968
|
recordFrame({ frameTimeMs = averageMs } = {}) {
|
|
2589
2969
|
const sample = Number.isFinite(Number(frameTimeMs)) ? Number(frameTimeMs) : averageMs;
|
|
2590
2970
|
frameSamples += 1;
|
|
2591
|
-
averageMs =
|
|
2971
|
+
averageMs = clamp2((averageMs * (frameSamples - 1) + sample) / frameSamples);
|
|
2592
2972
|
const fps = 1e3 / averageMs;
|
|
2593
2973
|
pressureLevel = sample > target.downgradeFrameTimeMs ? "degrade" : pressureLevel === "degrade" && sample <= target.upgradeFrameTimeMs ? "stable" : pressureLevel;
|
|
2594
2974
|
return {
|
|
@@ -3269,14 +3649,14 @@ function injectStyles() {
|
|
|
3269
3649
|
`;
|
|
3270
3650
|
document.head.appendChild(style);
|
|
3271
3651
|
}
|
|
3272
|
-
function
|
|
3652
|
+
function clamp(value, min, max) {
|
|
3273
3653
|
return Math.max(min, Math.min(max, value));
|
|
3274
3654
|
}
|
|
3275
3655
|
function mix(a, b, t) {
|
|
3276
3656
|
return a + (b - a) * t;
|
|
3277
3657
|
}
|
|
3278
3658
|
function smoothstep(min, max, value) {
|
|
3279
|
-
const t =
|
|
3659
|
+
const t = clamp((value - min) / Math.max(1e-4, max - min), 0, 1);
|
|
3280
3660
|
return t * t * (3 - 2 * t);
|
|
3281
3661
|
}
|
|
3282
3662
|
function pseudoRandom(seed) {
|
|
@@ -3360,10 +3740,10 @@ function projectPoint(point, camera, viewport) {
|
|
|
3360
3740
|
};
|
|
3361
3741
|
}
|
|
3362
3742
|
function colorToRgba(color, alpha = 1) {
|
|
3363
|
-
const r = Math.round(
|
|
3364
|
-
const g = Math.round(
|
|
3365
|
-
const b = Math.round(
|
|
3366
|
-
return `rgba(${r}, ${g}, ${b}, ${
|
|
3743
|
+
const r = Math.round(clamp(color.r, 0, 1) * 255);
|
|
3744
|
+
const g = Math.round(clamp(color.g, 0, 1) * 255);
|
|
3745
|
+
const b = Math.round(clamp(color.b, 0, 1) * 255);
|
|
3746
|
+
return `rgba(${r}, ${g}, ${b}, ${clamp(alpha, 0, 1)})`;
|
|
3367
3747
|
}
|
|
3368
3748
|
function mixColor(a, b, t) {
|
|
3369
3749
|
return {
|
|
@@ -3437,12 +3817,12 @@ function projectShadowPoint(point, lightDir, planeY) {
|
|
|
3437
3817
|
return addVec3(point, scaleVec3(shadowDir, distance));
|
|
3438
3818
|
}
|
|
3439
3819
|
function shadeColor(base, normal, lightDir, heightBias = 0, accent = 0) {
|
|
3440
|
-
const diffuse =
|
|
3820
|
+
const diffuse = clamp(dotVec3(normalizeVec3(normal), lightDir), 0, 1);
|
|
3441
3821
|
const brightness = 0.24 + diffuse * 0.72 + heightBias * 0.08 + accent;
|
|
3442
3822
|
return {
|
|
3443
|
-
r:
|
|
3444
|
-
g:
|
|
3445
|
-
b:
|
|
3823
|
+
r: clamp(base.r * brightness, 0, 1),
|
|
3824
|
+
g: clamp(base.g * brightness, 0, 1),
|
|
3825
|
+
b: clamp(base.b * (brightness + 0.03), 0, 1)
|
|
3446
3826
|
};
|
|
3447
3827
|
}
|
|
3448
3828
|
function getMaterialSeed(materialName) {
|
|
@@ -3480,13 +3860,13 @@ function applyMaterialDetail(color, material, worldCenter, normal, surfaceType)
|
|
|
3480
3860
|
const sample = worldCenter.x * 3.17 + worldCenter.y * 5.29 + worldCenter.z * 7.83 + getMaterialSeed(materialName) * 0.013;
|
|
3481
3861
|
const grain = (pseudoRandom(sample) - 0.5) * detailStrength;
|
|
3482
3862
|
const lowerSurface = smoothstep(7.5, -0.8, worldCenter.y);
|
|
3483
|
-
const verticalSurface = 1 -
|
|
3863
|
+
const verticalSurface = 1 - clamp(Math.abs(normal.y), 0, 1);
|
|
3484
3864
|
const materialLowerWear = /stone|concrete|plaster|paint|wood|timber|plank|crate/.test(materialName.toLowerCase()) ? lowerSurface * verticalSurface * 0.055 : 0;
|
|
3485
3865
|
const wetlineWear = surfaceType === "ship" && worldCenter.y < 0.72 ? smoothstep(0.72, -0.1, worldCenter.y) * 0.05 : 0;
|
|
3486
3866
|
return {
|
|
3487
|
-
r:
|
|
3488
|
-
g:
|
|
3489
|
-
b:
|
|
3867
|
+
r: clamp(color.r * (1 + grain) - materialLowerWear - wetlineWear, 0, 1),
|
|
3868
|
+
g: clamp(color.g * (1 + grain * 0.82) - materialLowerWear * 0.9 - wetlineWear, 0, 1),
|
|
3869
|
+
b: clamp(color.b * (1 + grain * 0.62) - materialLowerWear * 0.68 - wetlineWear * 0.75, 0, 1)
|
|
3490
3870
|
};
|
|
3491
3871
|
}
|
|
3492
3872
|
function buildCamera(state, canvas) {
|
|
@@ -3912,7 +4292,7 @@ function resizeCanvasToDisplaySize(canvas, state) {
|
|
|
3912
4292
|
const deviceScale = readPositiveNumber(globalThis.devicePixelRatio, 1);
|
|
3913
4293
|
const requestedScale = readPositiveNumber(state.renderScale, deviceScale);
|
|
3914
4294
|
const maxScale = state.captureMode ? 2 : 1.5;
|
|
3915
|
-
let scale =
|
|
4295
|
+
let scale = clamp(requestedScale, 1, maxScale);
|
|
3916
4296
|
const pixelBudget = state.captureMode ? CAPTURE_CANVAS_PIXEL_BUDGET : DEFAULT_CANVAS_WIDTH * DEFAULT_CANVAS_HEIGHT * 1.5;
|
|
3917
4297
|
const projectedPixels = width * height * scale * scale;
|
|
3918
4298
|
if (projectedPixels > pixelBudget) {
|
|
@@ -4097,12 +4477,12 @@ function satisfyClothConstraint(clothState, constraint) {
|
|
|
4097
4477
|
}
|
|
4098
4478
|
}
|
|
4099
4479
|
function advanceShowcaseClothSimulationState(clothState, options = {}) {
|
|
4100
|
-
const dt =
|
|
4480
|
+
const dt = clamp(options.dt ?? 1 / 60, 1 / 240, 1 / 18);
|
|
4101
4481
|
const time = readVisualNumber(options.time, 0);
|
|
4102
4482
|
const flagMotion = readVisualNumber(options.flagMotion, 0.92);
|
|
4103
4483
|
const waveInfluence = readVisualNumber(options.waveInfluence, 0);
|
|
4104
4484
|
const wrinkleLayers = Math.max(1, clothState.representation.mesh?.wrinkleLayers ?? 2);
|
|
4105
|
-
const solverIterations =
|
|
4485
|
+
const solverIterations = clamp(
|
|
4106
4486
|
Math.round(clothState.representation.mesh?.solverIterations ?? 6),
|
|
4107
4487
|
2,
|
|
4108
4488
|
10
|
|
@@ -4345,7 +4725,7 @@ function buildWaterMotionEffects(state) {
|
|
|
4345
4725
|
impulse.z
|
|
4346
4726
|
),
|
|
4347
4727
|
radius,
|
|
4348
|
-
opacity:
|
|
4728
|
+
opacity: clamp(impulse.life * 0.28, 0.08, 0.3)
|
|
4349
4729
|
});
|
|
4350
4730
|
});
|
|
4351
4731
|
for (const ship of state.ships) {
|
|
@@ -4377,7 +4757,7 @@ function buildWaterMotionEffects(state) {
|
|
|
4377
4757
|
}
|
|
4378
4758
|
wakeTrails.push(
|
|
4379
4759
|
Object.freeze({
|
|
4380
|
-
opacity:
|
|
4760
|
+
opacity: clamp(0.18 + speed * 0.09, 0.22, 0.46),
|
|
4381
4761
|
points: Object.freeze(points)
|
|
4382
4762
|
})
|
|
4383
4763
|
);
|
|
@@ -4591,7 +4971,7 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
|
|
|
4591
4971
|
const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
|
|
4592
4972
|
const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
|
|
4593
4973
|
const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
|
|
4594
|
-
ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${
|
|
4974
|
+
ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
|
|
4595
4975
|
ctx.beginPath();
|
|
4596
4976
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
4597
4977
|
ctx.fill();
|
|
@@ -4643,7 +5023,7 @@ function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, s
|
|
|
4643
5023
|
if (state.collisionFlash > 0.01) {
|
|
4644
5024
|
ctx.fillStyle = visuals.collisionFlash.replace(
|
|
4645
5025
|
/[\d.]+\)$/u,
|
|
4646
|
-
`${
|
|
5026
|
+
`${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
|
|
4647
5027
|
);
|
|
4648
5028
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
4649
5029
|
}
|
|
@@ -4662,7 +5042,7 @@ function resolveLocalLightContribution(triangle, lightSources) {
|
|
|
4662
5042
|
continue;
|
|
4663
5043
|
}
|
|
4664
5044
|
const lightDir = normalizeVec3(delta);
|
|
4665
|
-
const facing =
|
|
5045
|
+
const facing = clamp(dotVec3(normal, lightDir), 0, 1);
|
|
4666
5046
|
const response = attenuation * (0.18 + facing * 0.82);
|
|
4667
5047
|
const glowColor = source.glowColor ?? source.coreColor ?? { r: 1, g: 0.72, b: 0.4 };
|
|
4668
5048
|
contribution.r += glowColor.r * response * 0.32;
|
|
@@ -4684,31 +5064,31 @@ function drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, sha
|
|
|
4684
5064
|
triangle.baseColor,
|
|
4685
5065
|
surfaceNormal,
|
|
4686
5066
|
lightDir,
|
|
4687
|
-
|
|
5067
|
+
clamp((triangle.worldCenter.y + 3) / 10, 0, 1),
|
|
4688
5068
|
triangle.accent
|
|
4689
5069
|
);
|
|
4690
5070
|
const reflection = reflectionStrength * (triangle.reflection ?? 0);
|
|
4691
5071
|
const viewDir = normalizeVec3(subVec3(camera.eye, triangle.worldCenter));
|
|
4692
5072
|
const reflectedLight = reflectVec3(scaleVec3(lightDir, -1), surfaceNormal);
|
|
4693
|
-
const gloss = mix(0.78, 0.14,
|
|
4694
|
-
const specularPower = mix(26, 7,
|
|
4695
|
-
const specular = Math.pow(
|
|
5073
|
+
const gloss = mix(0.78, 0.14, clamp(material.roughness ?? 0.88, 0, 1)) + (material.metallic ?? 0) * 0.18;
|
|
5074
|
+
const specularPower = mix(26, 7, clamp(material.roughness ?? 0.88, 0, 1));
|
|
5075
|
+
const specular = Math.pow(clamp(dotVec3(reflectedLight, viewDir), 0, 1), specularPower) * gloss;
|
|
4696
5076
|
const emissive = material.emissive ?? { r: 0, g: 0, b: 0 };
|
|
4697
5077
|
const localLight = resolveLocalLightContribution(triangle, localLights);
|
|
4698
5078
|
const occlusion = triangle.surfaceType === "water" ? shadowStrength * 0.018 : shadowStrength * 0.04;
|
|
4699
5079
|
const detailed = applyMaterialDetail(
|
|
4700
5080
|
{
|
|
4701
|
-
r:
|
|
5081
|
+
r: clamp(
|
|
4702
5082
|
shaded.r + reflection * 0.08 + specular * 0.16 + emissive.r * 0.42 + localLight.r - occlusion,
|
|
4703
5083
|
0,
|
|
4704
5084
|
1
|
|
4705
5085
|
),
|
|
4706
|
-
g:
|
|
5086
|
+
g: clamp(
|
|
4707
5087
|
shaded.g + reflection * 0.08 + specular * 0.16 + emissive.g * 0.42 + localLight.g - occlusion,
|
|
4708
5088
|
0,
|
|
4709
5089
|
1
|
|
4710
5090
|
),
|
|
4711
|
-
b:
|
|
5091
|
+
b: clamp(
|
|
4712
5092
|
shaded.b + reflection * 0.16 + specular * 0.22 + emissive.b * 0.46 + localLight.b - occlusion * 0.5,
|
|
4713
5093
|
0,
|
|
4714
5094
|
1
|
|
@@ -4740,7 +5120,7 @@ function renderProjectedShadow(ctx, worldPoints, camera, viewport, lightDir, opt
|
|
|
4740
5120
|
}
|
|
4741
5121
|
ctx.save();
|
|
4742
5122
|
ctx.globalCompositeOperation = "multiply";
|
|
4743
|
-
ctx.fillStyle = options.color ?? `rgba(12, 24, 36, ${
|
|
5123
|
+
ctx.fillStyle = options.color ?? `rgba(12, 24, 36, ${clamp(options.alpha ?? 0.16, 0, 0.5)})`;
|
|
4744
5124
|
ctx.shadowColor = options.color ?? "rgba(12, 24, 36, 0.22)";
|
|
4745
5125
|
ctx.shadowBlur = options.blur ?? 18;
|
|
4746
5126
|
ctx.beginPath();
|
|
@@ -4977,7 +5357,7 @@ function resolveShipRoute(ship, state, radius) {
|
|
|
4977
5357
|
const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
|
|
4978
5358
|
const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
|
|
4979
5359
|
const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
|
|
4980
|
-
return vec3(targetX, 0,
|
|
5360
|
+
return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
|
|
4981
5361
|
}
|
|
4982
5362
|
function updateShipMotion(state, ship, dt, shipModel) {
|
|
4983
5363
|
const physics = shipModel.physics;
|
|
@@ -5090,7 +5470,7 @@ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
|
|
|
5090
5470
|
a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
|
|
5091
5471
|
b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
|
|
5092
5472
|
const tangentSpeed = dotVec3(relativeVelocity, tangent);
|
|
5093
|
-
const frictionMagnitude =
|
|
5473
|
+
const frictionMagnitude = clamp(
|
|
5094
5474
|
-tangentSpeed / Math.max(1e-4, invMassSum),
|
|
5095
5475
|
-impulseMagnitude * 0.16,
|
|
5096
5476
|
impulseMagnitude * 0.16
|
|
@@ -5111,14 +5491,14 @@ function resolveShipCollision(state, a, b, shipModelA, shipModelB) {
|
|
|
5111
5491
|
state.waveImpulses.push({
|
|
5112
5492
|
x: contactPoint.x,
|
|
5113
5493
|
z: contactPoint.z,
|
|
5114
|
-
strength:
|
|
5494
|
+
strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
|
|
5115
5495
|
radius: 0.9 + penetration * 1.4,
|
|
5116
5496
|
life: 1
|
|
5117
5497
|
});
|
|
5118
5498
|
state.collisionCount += 1;
|
|
5119
5499
|
state.collisionFlash = Math.max(
|
|
5120
5500
|
state.collisionFlash,
|
|
5121
|
-
|
|
5501
|
+
clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
|
|
5122
5502
|
);
|
|
5123
5503
|
a.collisionCooldown = 0.2;
|
|
5124
5504
|
b.collisionCooldown = 0.2;
|
|
@@ -5234,8 +5614,8 @@ function renderSprays(ctx, sprays, camera, viewport) {
|
|
|
5234
5614
|
if (!projected) {
|
|
5235
5615
|
continue;
|
|
5236
5616
|
}
|
|
5237
|
-
const radius =
|
|
5238
|
-
ctx.fillStyle = `rgba(225, 243, 250, ${
|
|
5617
|
+
const radius = clamp(1 / projected.depth * 260, 1.5, 7.5);
|
|
5618
|
+
ctx.fillStyle = `rgba(225, 243, 250, ${clamp(spray.life / 1.6, 0, 0.9)})`;
|
|
5239
5619
|
ctx.beginPath();
|
|
5240
5620
|
ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
|
|
5241
5621
|
ctx.fill();
|
|
@@ -5345,7 +5725,7 @@ function renderDirectLightGlow(ctx, source, camera, viewport) {
|
|
|
5345
5725
|
if (!projected) {
|
|
5346
5726
|
return;
|
|
5347
5727
|
}
|
|
5348
|
-
const radius =
|
|
5728
|
+
const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
|
|
5349
5729
|
const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
|
|
5350
5730
|
halo.addColorStop(0, colorToRgba(source.coreColor, 0.98));
|
|
5351
5731
|
halo.addColorStop(0.5, colorToRgba(source.glowColor, 0.42));
|
|
@@ -5367,7 +5747,7 @@ function renderWaterLightReflection(ctx, source, state, camera, viewport) {
|
|
|
5367
5747
|
if (!projected) {
|
|
5368
5748
|
return;
|
|
5369
5749
|
}
|
|
5370
|
-
const radius =
|
|
5750
|
+
const radius = clamp(1 / projected.depth * 420 * source.glowScale, 4, 34);
|
|
5371
5751
|
const waterline = sampleWave(state, source.point.x, source.point.z, state.time) * 0.22;
|
|
5372
5752
|
const reflectedPoint = vec3(
|
|
5373
5753
|
source.point.x,
|
|
@@ -5456,14 +5836,14 @@ function renderLighthouseBeam(ctx, state, camera, viewport, visuals) {
|
|
|
5456
5836
|
2,
|
|
5457
5837
|
projectedSource.x,
|
|
5458
5838
|
projectedSource.y,
|
|
5459
|
-
|
|
5839
|
+
clamp(beamLength * 0.22, 18, 80)
|
|
5460
5840
|
);
|
|
5461
5841
|
core.addColorStop(0, colorToRgba(visuals.torchCore, 0.58));
|
|
5462
5842
|
core.addColorStop(0.5, colorToRgba(visuals.torchGlow, 0.18));
|
|
5463
5843
|
core.addColorStop(1, colorToRgba(visuals.torchGlow, 0));
|
|
5464
5844
|
ctx.fillStyle = core;
|
|
5465
5845
|
ctx.beginPath();
|
|
5466
|
-
ctx.arc(projectedSource.x, projectedSource.y,
|
|
5846
|
+
ctx.arc(projectedSource.x, projectedSource.y, clamp(beamLength * 0.18, 14, 64), 0, Math.PI * 2);
|
|
5467
5847
|
ctx.fill();
|
|
5468
5848
|
ctx.restore();
|
|
5469
5849
|
}
|
|
@@ -5513,7 +5893,7 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
|
5513
5893
|
}
|
|
5514
5894
|
const averageDepth = projected.reduce((total, entry) => total + entry.projected.depth, 0) / projected.length;
|
|
5515
5895
|
const averageWidth = projected.reduce((total, entry) => total + entry.width, 0) / projected.length;
|
|
5516
|
-
const baseWidth =
|
|
5896
|
+
const baseWidth = clamp(averageWidth / Math.max(0.25, averageDepth) * 180, 1.6, 5.4);
|
|
5517
5897
|
ctx.strokeStyle = `rgba(146, 194, 236, ${wake.opacity * 0.52})`;
|
|
5518
5898
|
ctx.lineWidth = baseWidth * 1.9;
|
|
5519
5899
|
ctx.lineCap = "round";
|
|
@@ -5559,7 +5939,7 @@ function renderWaterMotionEffects(ctx, effects, camera, viewport) {
|
|
|
5559
5939
|
const radiusX = Math.hypot(xAxis.x - center.x, xAxis.y - center.y);
|
|
5560
5940
|
const radiusY = Math.hypot(zAxis.x - center.x, zAxis.y - center.y);
|
|
5561
5941
|
ctx.strokeStyle = `rgba(216, 235, 255, ${ring.opacity})`;
|
|
5562
|
-
ctx.lineWidth =
|
|
5942
|
+
ctx.lineWidth = clamp((radiusX + radiusY) * 0.02, 1, 3.1);
|
|
5563
5943
|
ctx.beginPath();
|
|
5564
5944
|
ctx.ellipse(center.x, center.y, radiusX, radiusY, 0, 0, Math.PI * 2);
|
|
5565
5945
|
ctx.stroke();
|