feat(web): finish RE 5000PBR lesson video and fix non-ASCII GLB import
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

- 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:
mes123456
2026-11-12 21:49:24 +08:00
parent a078648297
commit d5dd647881
276 changed files with 9632 additions and 314 deletions

View File

@@ -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`);