Files
workinf_Blender_Wasm/tools/web/generate-pbr-tutorial-studio.py
mes123456 d5dd647881
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
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>
2026-11-12 21:49:24 +08:00

162 lines
6.3 KiB
Python

"""Generate the PBR tutorial studio scene with desktop Blender.
Usage:
build_blender_native/bin/blender --background --factory-startup \
--python tools/web/generate-pbr-tutorial-studio.py -- \
web/app/public/tutorial/pbr/studio.blend \
tests/golden/PBR_TUTORIAL-001/studio-build.json
"""
import hashlib
import json
import pathlib
import sys
import bpy
from mathutils import Vector
def sha256_file(path: pathlib.Path) -> str:
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 add_area_light(name, location, energy, size):
# Point lights match the bounded three.js PBR renderer without needing
# RectAreaLight uniform support; the original size becomes a soft falloff.
bpy.ops.object.light_add(type="POINT", location=location)
light = bpy.context.object
light.name = name
light.data.energy = energy
light.data.shadow_soft_size = size * 0.5
light.rotation_euler = (-Vector(location)).to_track_quat("-Z", "Y").to_euler()
return light
def main(output_blend, report_path):
output = pathlib.Path(output_blend).resolve()
report = pathlib.Path(report_path).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
report.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "PBR Studio"
bpy.ops.object.camera_add(location=(7.2, -7.2, 5.4))
camera = bpy.context.object
camera.name = "PBR Studio Camera"
camera.data.name = "PBR Studio Camera"
camera.data.type = "PERSP"
camera.data.lens = 55
camera.data.sensor_width = 36
camera.data.sensor_fit = "HORIZONTAL"
camera.data.clip_start = 0.01
camera.data.clip_end = 1000
camera.rotation_euler = (-camera.location).to_track_quat("-Z", "Y").to_euler()
scene.camera = camera
key = add_area_light("PBR Studio Key", (3.2, -3.2, 4.0), 900, 4.0)
fill = add_area_light("PBR Studio Fill", (-4.0, -2.0, 2.0), 500, 5.0)
rim = add_area_light("PBR Studio Rim", (0.0, 4.0, 3.0), 700, 3.0)
bpy.ops.object.light_add(type="SUN", location=(4.0, -4.0, 6.0))
sun = bpy.context.object
sun.name = "PBR Studio Sun"
sun.data.energy = 30.0
sun.data.angle = 0.15
sun.rotation_euler = (-sun.location).to_track_quat("-Z", "Y").to_euler()
world = bpy.data.worlds.new("PBR Studio World")
world.use_nodes = True
background = world.node_tree.nodes.get("Background")
background.inputs["Color"].default_value = (0.045, 0.05, 0.06, 1.0)
background.inputs["Strength"].default_value = 1.4
# Packed equirectangular studio environment: point lights alone leave
# metallic materials almost black in the bounded MeshPhysicalMaterial path.
width, height = 512, 256
pixels = [0.0] * (width * height * 4)
for y in range(height):
v = y / (height - 1)
base = 0.35 + 0.65 * (v ** 1.2)
for x in range(width):
u = x / (width - 1)
glow = 0.0
for cx, cy, radius, strength in (
(0.20, 0.78, 0.12, 5.0),
(0.52, 0.85, 0.10, 4.0),
(0.82, 0.70, 0.13, 3.0),
):
dx = (u - cx) * 2.0
dy = (v - cy) * 1.4
glow += strength * max(0.0, 1.0 - (dx * dx + dy * dy) / (radius * radius))
value = min(8.0, base + glow)
index = (y * width + x) * 4
pixels[index] = value
pixels[index + 1] = value * 1.02
pixels[index + 2] = value * 1.06
pixels[index + 3] = 1.0
environment = bpy.data.images.new("PBR Studio Environment", width=width, height=height, alpha=False)
environment.pixels = pixels
environment.filepath_raw = str(output.parent / "_studio-environment.png")
environment.file_format = "PNG"
environment.save()
environment.pack()
pathlib.Path(environment.filepath_raw).unlink(missing_ok=True)
environment_node = world.node_tree.nodes.new("ShaderNodeTexEnvironment")
environment_node.name = "PBR Studio Environment"
environment_node.image = environment
world.node_tree.links.new(environment_node.outputs["Color"], background.inputs["Color"])
scene.world = world
bpy.ops.mesh.primitive_plane_add(size=20.0, location=(0.0, 0.0, -1.0))
ground = bpy.context.object
ground.name = "PBR Studio Ground"
ground.data.name = "PBR Studio Ground Mesh"
material = bpy.data.materials.new("PBR Studio Ground Material")
material.use_nodes = True
principled = material.node_tree.nodes.get("Principled BSDF")
principled.inputs["Base Color"].default_value = (0.32, 0.32, 0.33, 1.0)
principled.inputs["Roughness"].default_value = 0.65
principled.inputs["Metallic"].default_value = 0.0
ground.data.materials.append(material)
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.view_settings.look = "None"
bpy.ops.wm.save_as_mainfile(filepath=str(output), compress=True)
lights = [light.name for light in (key, fill, rim, sun)]
payload = {
"schemaVersion": 1,
"generator": "tools/web/generate-pbr-tutorial-studio.py",
"blenderVersion": ".".join(str(value) for value in bpy.app.version),
"blenderExecutable": bpy.app.binary_path,
"blend": {
"path": str(output),
"sha256": sha256_file(output),
"byteLength": output.stat().st_size,
},
"objects": sorted(object.name for object in scene.objects),
"camera": camera.name,
"lights": lights,
"world": world.name,
"ground": ground.name,
}
report.write_text(json.dumps(payload, indent=2, ensure_ascii=True) + "\n", encoding="utf-8", newline="\n")
print(json.dumps({"studio": str(output), "sha256": payload["blend"]["sha256"], "objects": payload["objects"]}))
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
if len(arguments) != 2:
raise SystemExit("usage: blender --background --factory-startup --python generate-pbr-tutorial-studio.py -- BLEND REPORT")
main(arguments[0], arguments[1])