feat(web): finish RE 5000PBR lesson video and fix non-ASCII GLB import
- Add 11:07 Chinese-narrated lesson video, VTT, poster and 13 chapter screenshots. - Regenerate 20-render WebGL2 matrix and 15 operation captures on RTX 4060. - Fix WASM staging for non-ASCII import filenames (猴头.glb); rebuild web_engine. - Harden lesson video recording pipeline; fix stale lesson-link E2E selector. - Add RE PBR derived libraries, lesson page, evidence and status report. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ export const MAX_GPU_TEXTURE_DIMENSION = 16_384;
|
||||
export const MAX_GPU_TEXTURE_ASSETS = 256;
|
||||
export const MAX_UDIM_TILES = 64;
|
||||
|
||||
export type GPUTextureUsage = "BASE_COLOR" | "NORMAL" | "METALLIC" | "ROUGHNESS" | "EMISSIVE" | "ALPHA" | "BUMP" | "DATA" | "ENVIRONMENT" | "UDIM_TILE";
|
||||
export type GPUTextureUsage = "BASE_COLOR" | "NORMAL" | "METALLIC" | "ROUGHNESS" | "EMISSIVE" | "ALPHA" | "BUMP" | "HEIGHT" | "DATA" | "ENVIRONMENT" | "UDIM_TILE";
|
||||
export type GPUTextureColorSpace = "SRGB" | "NON_COLOR" | "LINEAR";
|
||||
|
||||
export interface GPUTextureAssetRequest {
|
||||
@@ -108,22 +108,35 @@ function materialUsages(materials: MaterialIR[]): Map<string, Set<GPUTextureUsag
|
||||
usages.set(imageId, current);
|
||||
};
|
||||
for (const material of materials) {
|
||||
if (material.normalImageId) add(material.normalImageId, "NORMAL");
|
||||
for (const binding of material.textureBindings ?? []) add(binding.imageId, binding.usage === "EMISSION" ? "EMISSIVE" : binding.usage);
|
||||
const bound = new Set<string>();
|
||||
if (material.normalImageId) {
|
||||
bound.add(material.normalImageId);
|
||||
add(material.normalImageId, "NORMAL");
|
||||
}
|
||||
for (const binding of material.textureBindings ?? []) {
|
||||
bound.add(binding.imageId);
|
||||
add(binding.imageId, binding.usage === "EMISSION" ? "EMISSIVE" : binding.usage);
|
||||
}
|
||||
for (const imageId of material.imageIds ?? []) {
|
||||
if (!material.textureBindings?.some((binding) => binding.imageId === imageId)) add(imageId, imageId === material.normalImageId ? "NORMAL" : "BASE_COLOR");
|
||||
if (bound.has(imageId)) continue;
|
||||
add(imageId, imageId === material.normalImageId ? "NORMAL" : "BASE_COLOR");
|
||||
}
|
||||
for (const node of material.nodes ?? []) {
|
||||
if (node.type === "IMAGE_TEXTURE" && node.imageId) add(node.imageId, node.imageId === material.normalImageId ? "NORMAL" : "BASE_COLOR");
|
||||
if (node.type === "IMAGE_TEXTURE" && node.imageId && !bound.has(node.imageId)) {
|
||||
add(node.imageId, node.imageId === material.normalImageId ? "NORMAL" : "BASE_COLOR");
|
||||
}
|
||||
}
|
||||
}
|
||||
return usages;
|
||||
}
|
||||
|
||||
function requestForImage(image: ImageIR, usage: GPUTextureUsage): GPUTextureAssetRequest[] {
|
||||
const colorSpace: GPUTextureColorSpace = image.colorSpace ?? (usage === "BASE_COLOR" || usage === "EMISSIVE" ? "SRGB" : usage === "ENVIRONMENT" ? "LINEAR" : "NON_COLOR");
|
||||
if (image.tiles?.length) {
|
||||
return image.tiles.filter((tile) => tile.packed).map((tile) => ({
|
||||
const colorSpace: GPUTextureColorSpace = usage === "HEIGHT"
|
||||
? "NON_COLOR"
|
||||
: image.colorSpace ?? (usage === "BASE_COLOR" || usage === "EMISSIVE" ? "SRGB" : usage === "ENVIRONMENT" ? "LINEAR" : "NON_COLOR");
|
||||
const tiles = image.tiles;
|
||||
if (tiles && tiles.length > 1) {
|
||||
return tiles.filter((tile) => tile.packed).map((tile) => ({
|
||||
assetId: tile.assetId,
|
||||
imageId: image.id,
|
||||
mimeType: imageMimeType(tile.mimeType),
|
||||
@@ -134,6 +147,19 @@ function requestForImage(image: ImageIR, usage: GPUTextureUsage): GPUTextureAsse
|
||||
tileNumber: tile.number,
|
||||
}));
|
||||
}
|
||||
if (tiles && tiles.length === 1) {
|
||||
const tile = tiles[0];
|
||||
if (!tile.packed) return [];
|
||||
return [{
|
||||
assetId: tile.assetId,
|
||||
imageId: image.id,
|
||||
mimeType: imageMimeType(tile.mimeType),
|
||||
width: image.width || tile.width,
|
||||
height: image.height || tile.height,
|
||||
usage,
|
||||
colorSpace,
|
||||
}];
|
||||
}
|
||||
if (!image.packed) return [];
|
||||
return [{
|
||||
assetId: image.assetId,
|
||||
@@ -162,6 +188,26 @@ export function collectGPUTextureAssetRequests(snapshot: SceneSnapshotIR): GPUTe
|
||||
return [...unique.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of GPU texture assets the packed-asset fallback will produce. This
|
||||
* includes images that have no MaterialTextureBinding yet (the App adds a
|
||||
* BASE_COLOR/ENVIRONMENT fallback for them) so readiness cannot release early.
|
||||
*/
|
||||
export function expectedGPUTextureAssetCount(snapshot: SceneSnapshotIR): number {
|
||||
const requests = collectGPUTextureAssetRequests(snapshot);
|
||||
const represented = new Set(requests.map((request) => request.assetId));
|
||||
let count = requests.length;
|
||||
for (const image of snapshot.images) {
|
||||
if (image.tiles?.length) {
|
||||
for (const tile of image.tiles) if (tile.packed && !represented.has(tile.assetId)) count += 1;
|
||||
}
|
||||
else if (image.packed && !represented.has(image.assetId)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function createUDIMManifest(image: ImageIR): UDIMManifest {
|
||||
if (image.sourceKind !== "TILED" || !image.tiles?.length || image.tiles.length > MAX_UDIM_TILES) {
|
||||
throw new RenderAssetValidationError("UDIM_MANIFEST_INVALID", `Image ${image.id} does not have a bounded UDIM tile set`);
|
||||
|
||||
Reference in New Issue
Block a user