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:
130
tools/web/generate-pbr-tutorial-renders.py
Normal file
130
tools/web/generate-pbr-tutorial-renders.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Render the 12 tutorial material/model combinations with desktop Blender.
|
||||
|
||||
Usage:
|
||||
build_blender_native/bin/blender --background --factory-startup \
|
||||
--python tools/web/generate-pbr-tutorial-renders.py -- \
|
||||
web/app/public/tutorial/pbr/studio.blend \
|
||||
web/app/public/assets/pbr-library/pbr-library.blend \
|
||||
web/app/public/tutorial/pbr/renders
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
MODELS = [
|
||||
("PBR Suzanne", "suzanne"),
|
||||
("PBR Cube", "cube"),
|
||||
("PBR Torus", "torus"),
|
||||
]
|
||||
MATERIALS = [
|
||||
("PBR Metal Brushed", "metal-brushed"),
|
||||
("PBR Glass", "glass"),
|
||||
("PBR Wood", "wood"),
|
||||
("PBR Emission Cyan", "emission-cyan"),
|
||||
]
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def append_library(blend_path):
|
||||
with bpy.data.libraries.load(str(blend_path), link=False) as (data_from, data_to):
|
||||
data_to.objects = list(data_from.objects)
|
||||
data_to.materials = list(data_from.materials)
|
||||
data_to.images = list(data_from.images)
|
||||
return data_to.objects, data_to.materials
|
||||
|
||||
|
||||
def main(studio_path, library_path, renders_dir):
|
||||
studio = pathlib.Path(studio_path).resolve()
|
||||
library = pathlib.Path(library_path).resolve()
|
||||
out = pathlib.Path(renders_dir).resolve()
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=str(studio))
|
||||
scene = bpy.context.scene
|
||||
objects, materials = append_library(library)
|
||||
material_by_name = {material.name: material for material in materials if material}
|
||||
model_by_name = {}
|
||||
for obj in objects:
|
||||
if obj is None:
|
||||
continue
|
||||
scene.collection.objects.link(obj)
|
||||
model_by_name[obj.name] = obj
|
||||
obj.hide_render = True
|
||||
|
||||
scene.render.engine = "BLENDER_EEVEE"
|
||||
scene.render.resolution_x = 1024
|
||||
scene.render.resolution_y = 1024
|
||||
scene.render.resolution_percentage = 100
|
||||
scene.render.image_settings.file_format = "PNG"
|
||||
scene.render.image_settings.color_mode = "RGBA"
|
||||
scene.render.image_settings.color_depth = "8"
|
||||
scene.render.film_transparent = False
|
||||
scene.render.dither_intensity = 0
|
||||
scene.eevee.taa_render_samples = 64
|
||||
scene.view_settings.look = "None"
|
||||
|
||||
manifest = []
|
||||
for model_name, model_slug in MODELS:
|
||||
model = model_by_name[model_name]
|
||||
for material_name, material_slug in MATERIALS:
|
||||
for other in model_by_name.values():
|
||||
other.hide_render = other is not model
|
||||
material = material_by_name.get(material_name)
|
||||
if material is None:
|
||||
raise RuntimeError(f"missing material {material_name}")
|
||||
model.data.materials.clear()
|
||||
model.data.materials.append(material)
|
||||
png = out / f"{model_slug}-{material_slug}.png"
|
||||
jpg = out / f"{model_slug}-{material_slug}.jpg"
|
||||
scene.render.filepath = str(png)
|
||||
bpy.ops.render.render(write_still=True)
|
||||
scene.render.image_settings.file_format = "JPEG"
|
||||
scene.render.image_settings.color_mode = "RGB"
|
||||
scene.render.image_settings.quality = 90
|
||||
scene.render.filepath = str(jpg)
|
||||
bpy.ops.render.render(write_still=True)
|
||||
scene.render.image_settings.file_format = "PNG"
|
||||
scene.render.image_settings.color_mode = "RGBA"
|
||||
manifest.append({
|
||||
"model": model_name,
|
||||
"modelSlug": model_slug,
|
||||
"material": material_name,
|
||||
"materialSlug": material_slug,
|
||||
"png": str(png.relative_to(pathlib.Path(__file__).resolve().parents[2])).replace("\\", "/"),
|
||||
"jpg": str(jpg.relative_to(pathlib.Path(__file__).resolve().parents[2])).replace("\\", "/"),
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"bytes": png.stat().st_size,
|
||||
"sha256": sha256_file(png),
|
||||
})
|
||||
|
||||
(out / "manifest.json").write_text(
|
||||
json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"renderer": "DESKTOP_BLENDER_EEVEE",
|
||||
"blenderVersion": ".".join(str(value) for value in bpy.app.version),
|
||||
"renders": manifest,
|
||||
}, indent=2, ensure_ascii=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
print(json.dumps({"renders": len(manifest), "output": str(out)}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
if len(arguments) != 3:
|
||||
raise SystemExit("usage: blender --background --factory-startup --python generate-pbr-tutorial-renders.py -- STUDIO LIBRARY RENDERS")
|
||||
main(*arguments)
|
||||
Reference in New Issue
Block a user