colorpie.blend now has packed lighting and served as the master for the template and at this time there is no difference between the two. Confirmed webp creation, the results of which are being committed herein. Placed failed or poorly designed scripts in the attic for reference, with Google Gemini it started down a path of exporting a movie and then extracting &etc nonsense, that is when I returned to ChatGPT and we built render_colorpie_glb_to_webp.py

This commit is contained in:
jlpoole 2026-03-08 18:06:03 -07:00
commit 6e3137b30a
14 changed files with 417 additions and 0 deletions

View file

@ -0,0 +1,112 @@
#!/usr/bin/python
#
# Google Gemini 3/8/26
#
import bpy
import os
import sys
# 1. Handle Command Line Arguments
argv = sys.argv
if "--" not in argv:
print("Usage: blender -b file.blend -P script.py -- model.glb")
sys.exit()
glb_path = argv[argv.index("--") + 1:]
if not glb_path:
sys.exit()
glb_path = glb_path[0]
base_name = os.path.splitext(os.path.basename(glb_path))[0]
# 2. Preparation: Find the Parent and Clear Old Children
parent = bpy.data.objects.get("ColorpieParent")
# Delete old children of the parent so we don't render multiple donuts on top of each other
if parent:
for child in parent.children:
bpy.data.objects.remove(child, do_unlink=True)
# 3. Import the New GLB
bpy.ops.import_scene.gltf(filepath=glb_path)
# 4. Parent New Objects to the Animation "Coin"
# Newly imported objects are automatically 'selected'
imported_objs = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
for obj in imported_objs:
if parent:
obj.parent = parent
# Optional: Reset local transforms so it aligns perfectly with the parent
obj.location = (0, 0, 0)
# # 5. Render Settings
# scene = bpy.context.scene
# scene.render.image_settings.file_format = 'WEBP'
# scene.render.image_settings.quality = 95
# scene.render.film_transparent = False # Ensure black background from World nodes
# # 6. Render the "Stills" (Front and Back)
# # Front Face (1 second in)
# scene.frame_set(1)
# scene.render.filepath = os.path.abspath(f"front_{base_name}.webp")
# bpy.ops.render.render(write_still=True)
# # Back Face (Post-flip, e.g., Frame 144)
# scene.frame_set(144)
# scene.render.filepath = os.path.abspath(f"back_{base_name}.webp")
# bpy.ops.render.render(write_still=True)
# # 5. Render Settings (FFMPEG Workaround)
# scene = bpy.context.scene
# scene.render.image_settings.file_format = 'FFMPEG'
# scene.render.ffmpeg.format = 'QUICKTIME' # MOV container
# scene.render.ffmpeg.video_codec = 'PNG' # Use PNG compression inside the MOV
# 5. Render Settings (Blender 5.0 + Headless Workaround)
scene = bpy.context.scene
scene.render.image_settings.file_format = 'FFMPEG'
# Set the Container to Quicktime (MOV)
scene.render.ffmpeg.format = 'QUICKTIME'
# In Blender 5.0, use .codec instead of .video_codec
scene.render.ffmpeg.codec = 'PNG'
# Ensure it captures only the frame we want
scene.render.ffmpeg.constant_rate_factor = 'HIGH'
# # 6. Render the "Stills"
# # Note: Blender will add frame numbers to these filenames
# # Frame 1 (Front)
# scene.frame_set(1)
# scene.render.filepath = os.path.abspath(f"front_{base_name}")
# bpy.ops.render.render(write_still=True)
# # Frame 144 (Back)
# scene.frame_set(144)
# scene.render.filepath = os.path.abspath(f"back_{base_name}")
# bpy.ops.render.render(write_still=True)
# 6. Render the "Stills" as 1-frame movies
# Frame 1 (Front)
scene.frame_start = 1
scene.frame_end = 1
scene.render.filepath = os.path.abspath(f"front_{base_name}")
bpy.ops.render.render(animation=True)
# Frame 144 (Back)
scene.frame_start = 144
scene.frame_end = 144
scene.render.filepath = os.path.abspath(f"back_{base_name}")
bpy.ops.render.render(animation=True)
# 7. Render Full Video (Only for specific files)
if "full_gamut" in base_name:
# Set to Video format for the animation
scene.render.image_settings.file_format = 'FFMPEG'
scene.render.ffmpeg.format = 'MPEG4'
scene.render.ffmpeg.codec = 'H264'
scene.render.filepath = os.path.abspath(f"anim_{base_name}.mp4")
bpy.ops.render.render(animation=True)

View file

@ -0,0 +1,284 @@
#!/usr/bin/env python3
# 20260308 ChatGPT
# $Header$
#
# Example:
# blender-bin-5.0.0 -b /path/to/colorpie.blend \
# --python scripts/render_colorpie_glb_to_webp.py -- \
# /path/to/glb/colorpie_gamut_20260308_1303.glb \
# --outdir /path/to/out
#
# Optional:
# --parent ColorpieParent
# --front-frame 1
# --back-frame 144
# --quality 95
#
# This script assumes:
# 1) the .blend already has the correct camera, Eevee, world, lights, compositor, etc.
# 2) ColorpieParent is the animated top object
# 3) the incoming GLB is centered the same way as the original one
import bpy
import os
import sys
import time
def die(msg, code=1):
print(f"ERROR: {msg}", file=sys.stderr)
sys.exit(code)
def log(msg):
print(msg, flush=True)
def parse_args(argv):
if "--" in argv:
argv = argv[argv.index("--") + 1:]
else:
argv = []
glb_path = None
outdir = "."
parent_name = "ColorpieParent"
front_frame = 1
back_frame = 144
quality = 95
i = 0
pos = []
while i < len(argv):
a = argv[i]
if a == "--outdir":
i += 1
if i >= len(argv):
die("Missing value after --outdir")
outdir = argv[i]
elif a == "--parent":
i += 1
if i >= len(argv):
die("Missing value after --parent")
parent_name = argv[i]
elif a == "--front-frame":
i += 1
if i >= len(argv):
die("Missing value after --front-frame")
front_frame = int(argv[i])
elif a == "--back-frame":
i += 1
if i >= len(argv):
die("Missing value after --back-frame")
back_frame = int(argv[i])
elif a == "--quality":
i += 1
if i >= len(argv):
die("Missing value after --quality")
quality = int(argv[i])
else:
pos.append(a)
i += 1
if not pos:
die("Usage: blender -b template.blend --python script.py -- input.glb [--outdir DIR]")
glb_path = pos[0]
if not os.path.exists(glb_path):
die(f"GLB not found: {glb_path}")
os.makedirs(outdir, exist_ok=True)
return {
"glb_path": glb_path,
"outdir": outdir,
"parent_name": parent_name,
"front_frame": front_frame,
"back_frame": back_frame,
"quality": quality,
}
def collect_descendants(obj):
out = []
stack = list(obj.children)
while stack:
cur = stack.pop()
out.append(cur)
stack.extend(cur.children)
return out
def delete_descendants(parent):
victims = collect_descendants(parent)
if not victims:
log("No existing children beneath parent; nothing to delete")
return
# Unlink carefully from children upward
for obj in sorted(victims, key=lambda o: len(o.children), reverse=True):
try:
bpy.data.objects.remove(obj, do_unlink=True)
except Exception as exc:
log(f"WARNING: failed removing {obj.name}: {exc}")
log(f"Deleted {len(victims)} descendant object(s) under {parent.name}")
def import_glb_and_get_new_objects(glb_path):
before = set(bpy.data.objects.keys())
res = bpy.ops.import_scene.gltf(filepath=glb_path)
if "FINISHED" not in res:
die(f"GLB import failed: {glb_path}")
after = set(bpy.data.objects.keys())
new_names = sorted(after - before)
new_objs = [bpy.data.objects[name] for name in new_names]
if not new_objs:
die("Import succeeded, but no new objects were detected")
log(f"Imported {len(new_objs)} object(s) from {glb_path}")
return new_objs
def get_import_roots(imported_objs):
imported_set = set(imported_objs)
roots = [obj for obj in imported_objs if obj.parent not in imported_set]
if not roots:
roots = imported_objs[:]
return roots
def parent_roots_to_target(roots, parent):
for obj in roots:
# Preserve current world-space transform on parenting
world_matrix = obj.matrix_world.copy()
obj.parent = parent
obj.matrix_parent_inverse = parent.matrix_world.inverted()
obj.matrix_world = world_matrix
log(f"Parented {len(roots)} imported root object(s) to {parent.name}")
# def ensure_render_settings(scene, quality):
# scene.render.image_settings.file_format = 'WEBP'
# scene.render.image_settings.quality = quality
# scene.render.use_file_extension = True
# # Leave all your scene-specific Eevee/world/compositor settings alone.
# # The point is to use the .blend as-is.
# log(f"Render format set to WEBP, quality={quality}")
def ensure_render_settings(scene, quality):
isettings = scene.render.image_settings
# Blender 5.x split still vs video by media_type.
# Do not guess blindly; inspect the enum and choose the still-image mode.
media_prop = isettings.bl_rna.properties.get("media_type")
if media_prop is not None:
media_keys = {item.identifier for item in media_prop.enum_items}
if "IMAGE" in media_keys:
isettings.media_type = 'IMAGE'
elif "STILL" in media_keys:
isettings.media_type = 'STILL'
elif "VIDEO" in media_keys and len(media_keys) == 1:
raise RuntimeError(
f"Scene is locked to VIDEO media_type only: {sorted(media_keys)}"
)
fmt_keys = {
item.identifier
for item in isettings.bl_rna.properties["file_format"].enum_items
}
# Prefer WEBP if available, otherwise fall back to PNG.
if "WEBP" in fmt_keys:
isettings.file_format = 'WEBP'
if hasattr(isettings, "quality"):
isettings.quality = quality
ext = ".webp"
elif "PNG" in fmt_keys:
isettings.file_format = 'PNG'
ext = ".png"
else:
raise RuntimeError(
f"No usable still-image format available. file_format choices: {sorted(fmt_keys)}"
)
scene.render.use_file_extension = True
print(f"media_type = {getattr(isettings, 'media_type', '(none)')}")
print(f"file_format choices = {sorted(fmt_keys)}")
print(f"using still format = {isettings.file_format}")
return ext
def render_frame(scene, frame_no, outfile):
scene.frame_set(frame_no)
scene.render.filepath = outfile
log(f"Rendering frame {frame_no} -> {outfile}")
bpy.ops.render.render(write_still=True)
def main():
cfg = parse_args(sys.argv)
glb_path = cfg["glb_path"]
outdir = cfg["outdir"]
parent_name = cfg["parent_name"]
front_frame = cfg["front_frame"]
back_frame = cfg["back_frame"]
quality = cfg["quality"]
base_name = os.path.splitext(os.path.basename(glb_path))[0]
timestamp = time.strftime("%Y%m%d_%H%M")
scene = bpy.context.scene
parent = bpy.data.objects.get(parent_name)
if parent is None:
die(f"Parent object not found: {parent_name}")
log(f"Using parent: {parent.name}")
log(f"Using scene: {scene.name}")
log(f"Input GLB: {glb_path}")
delete_descendants(parent)
imported = import_glb_and_get_new_objects(glb_path)
roots = get_import_roots(imported)
parent_roots_to_target(roots, parent)
for obj in imported:
pname = obj.parent.name if obj.parent else "-"
print(f"IMPORTED: {obj.name:30s} type={obj.type:8s} parent={pname}")
# ensure_render_settings(scene, quality)
# front_out = os.path.join(outdir, f"{base_name}_frame001_{timestamp}.webp")
# back_out = os.path.join(outdir, f"{base_name}_frame144_{timestamp}.webp")
ext = ensure_render_settings(scene, quality)
front_out = os.path.join(outdir, f"{base_name}_frame001_{timestamp}{ext}")
back_out = os.path.join(outdir, f"{base_name}_frame144_{timestamp}{ext}")
render_frame(scene, front_frame, front_out)
render_frame(scene, back_frame, back_out)
log("Done")
log(f"Wrote: {front_out}")
log(f"Wrote: {back_out}")
if __name__ == "__main__":
main()