- 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>
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
"""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)
|