diff --git a/create_glb.py b/create_glb.py new file mode 100644 index 0000000..701a9dc --- /dev/null +++ b/create_glb.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# create_glb.py — Blender headless STL -> GLB +# +# Example: +# blender-bin-5.0.0 --background --python create_glb.py -- input.stl [output.glb] + +import bpy +import sys +import os +from mathutils import Vector + +def die(msg, rc=2): + print(f"ERROR: {msg}") + raise SystemExit(rc) + +# args after "--" +argv = sys.argv +argv = argv[argv.index("--") + 1:] if "--" in argv else [] + +if len(argv) == 1: + inp = argv[0] + base, _ = os.path.splitext(inp) + outp = base + ".glb" +elif len(argv) >= 2: + inp, outp = argv[0], argv[1] +else: + die("USAGE: blender --background --python create_glb.py -- input.stl [output.glb]") + +if not os.path.exists(inp): + die(f"Input not found: {inp}") + +# Empty scene +bpy.ops.wm.read_factory_settings(use_empty=True) + +# Import STL (Blender 4/5 operator) +res = bpy.ops.wm.stl_import(filepath=inp) +if 'FINISHED' not in res: + die(f"STL import failed for: {inp}") + +# Gather imported mesh objects +objs = [o for o in bpy.context.scene.objects if o.type == 'MESH'] +if not objs: + die("No mesh objects after import (unexpected)") + +# Compute combined bounding box center in world space +min_v = Vector(( 1e30, 1e30, 1e30)) +max_v = Vector((-1e30, -1e30, -1e30)) + +for o in objs: + # object bound_box is in local coords; transform to world + for corner in o.bound_box: + v = o.matrix_world @ Vector(corner) + min_v.x = min(min_v.x, v.x); min_v.y = min(min_v.y, v.y); min_v.z = min(min_v.z, v.z) + max_v.x = max(max_v.x, v.x); max_v.y = max(max_v.y, v.y); max_v.z = max(max_v.z, v.z) + +center = (min_v + max_v) * 0.5 + +# Translate all meshes so center is at origin +for o in objs: + o.location -= center + +# Export GLB +res = bpy.ops.export_scene.gltf( + filepath=outp, + export_format='GLB', + export_apply=True, +) +if 'FINISHED' not in res: + die(f"GLB export failed: {outp}") + +print(f"Wrote: {outp}") \ No newline at end of file diff --git a/creating_glbs.md b/creating_glbs.md new file mode 100644 index 0000000..df1a1d3 --- /dev/null +++ b/creating_glbs.md @@ -0,0 +1,14 @@ + + + +Here is a command to create a single glb: + + blender-bin-5.0.0 --background --python create_glb.py -- \ + /usr/local/src/Voron-Stealthburner/STLs/Stealthburner/'[o]_stealthburner_LED_carrier.stl' \ + /tmp/out.glb + +The git repository for Voron-Stealthburner was staged under /usr/local/src. The above command +selects a specific STL and then places the STL under /tmp. + +In a production mode, we want to be able to point to a directory tree of STLs and then +generate glb equivalents in the similar tree structure. \ No newline at end of file diff --git a/exercises/create_png_from_glb/README.md b/exercises/create_png_from_glb/README.md new file mode 100644 index 0000000..e69de29 diff --git a/exercises/create_png_profile/20260304_101254_Wed.png b/exercises/create_png_profile/20260304_101254_Wed.png new file mode 100644 index 0000000..23a84a7 Binary files /dev/null and b/exercises/create_png_profile/20260304_101254_Wed.png differ diff --git a/exercises/create_png_profile/20260304_101754_Wed.png b/exercises/create_png_profile/20260304_101754_Wed.png new file mode 100644 index 0000000..e9eb68e Binary files /dev/null and b/exercises/create_png_profile/20260304_101754_Wed.png differ diff --git a/exercises/create_png_profile/20260304_102737_Wed.png b/exercises/create_png_profile/20260304_102737_Wed.png new file mode 100644 index 0000000..37602b5 Binary files /dev/null and b/exercises/create_png_profile/20260304_102737_Wed.png differ diff --git a/exercises/create_png_profile/20260304_102919_Wed.png b/exercises/create_png_profile/20260304_102919_Wed.png new file mode 100644 index 0000000..9790d34 Binary files /dev/null and b/exercises/create_png_profile/20260304_102919_Wed.png differ diff --git a/exercises/create_png_profile/README.md b/exercises/create_png_profile/README.md new file mode 100644 index 0000000..a731784 --- /dev/null +++ b/exercises/create_png_profile/README.md @@ -0,0 +1,260 @@ +# Goal +Create a "profile", i.e. a JSON file, to be used by the script that mass converts *.glb to *.png + +## Introduction +This exercise only requires that you launch a small HTTP server in a console. Otherwise, everything involved is handled through the HTML page lab.html. You will interact with lab.html's 3D rendering of a glb file that is included with this project. + +## Steps +Open a browser or a new window of the browser (Ctrl-n in Firefox) and resize the browser to a small rectangle. You are reducing the size so as to mimic what the PNG cell in the manifest table will look like. For example, this reduced window +is 521 × 432 pixels. +![](20260304_101754_Wed.png) + +In a console: + + cd ~/work/Voron/voronstl/web + python3 -m http.server 8001 + +You should have a console that looks like this: + +![](20260304_101254_Wed.png) + +It is necessary to start the web server within the "web" directory as that directory +will be servers "root". + + +Visit: + + http://localhost:8001/lab.html + + +You will see a zoomed-in image: +![](Screenshot_20260304_102153_AM_Wed-1.png) + +Zoom out until the entire part fits within the window. + +Click the Controls bar to collapse the sub menus. +![](Screenshot_20260304_102435_AM_Wed.png) +Move the object to center it in the window: Shift + left mouse button. You want to have the entire part fit within the view and be cenetered. + +Click Controls bar to open the sub menus. Adjust the lighintensity to a high value, if not the maximum values. This will cause the image to go lighter allowing for contrast with shadows that help discern the part. +![](Screenshot_20260304_154311_PM_Wed.png) +Optional: Save the PNG for your own reference. + +Click "Export Profile" and save your current settings. + +![](20260304_102737_Wed.png) + +You now have a specification on sizing and angles which may work well for all of the other parts. Note: I took mine and applied the specifications saved above for a mass PNG creation and all the others looked very good. + +### Additional Information: +Here's what a JSON file looks like: + + + + + + + + + + + + +
ImageDescription
+ +
jlpoole@jp ~/work/Voron/voronstl/web $ jq . out/three_profile_20260304_102657.json
+{
+  "provenance": "lab.html exportProfile Wed Mar 04 2026 10:26:57 GMT-0800 (Pacific Standard Time)",
+  "output": {
+    "width": 500,
+    "height": 315,
+    "pixelRatio": 1
+  },
+  "scene": {
+    "background": 16777215
+  },
+  "camera": {
+    "type": "PerspectiveCamera",
+    "fov": 50,
+    "near": 0.1,
+    "far": 1000,
+    "position": [
+      11.93432933230491,
+      11.71721921868296,
+      13.265804443912849
+    ],
+    "up": [
+      0,
+      1,
+      0
+    ]
+  },
+  "controls": {
+    "target": [
+      2.075876663103527,
+      -2.147814989328729,
+      0.7122034073683343
+    ]
+  },
+  "renderParams": {
+    "wireframe": false,
+    "edgeAngle": 30,
+    "lightIntensity": 1.5
+  },
+  "lights": {
+    "directional": {
+      "position": [
+        5,
+        5,
+        5
+      ],
+      "intensity": 1.5
+    },
+    "ambient": {
+      "intensity": 0.6
+    }
+  }
+}
+jlpoole@jp ~/work/Voron/voronstl/web $ 
+
+
+ +## ChatGPT's analysis of what is important for consistency + +Here is a response from ChatGPT that summarizes what variables are captured and saved so that they can be used to repeat the same type of transformation of *.glb to PNG. + +## Response + +Good — now you’re at the point where it’s worth making the capture **repeatable** across a whole directory of GLBs. + +In practice, you want to “freeze” *everything that affects the pixels* and treat the GLB as the only variable. + +## What to save (so every PNG is consistent) + +### 1) Camera pose and projection + +Save: + +* `camera.position` (x,y,z) +* `camera.up` (x,y,z) +* `controls.target` (x,y,z) if you’re using OrbitControls +* `camera.fov`, `camera.near`, `camera.far` +* (optional) whether you used perspective vs orthographic + +These define “where the viewer is” and are the biggest determinant of repeatability. + +### 2) Output framing rules (per-model normalization) + +You need a deterministic rule for “how big the part appears in frame”, otherwise parts with different sizes will look random. + +Two common strategies: + +**A. Fit-to-frame (recommended for mass renders)** +For each GLB: + +* compute `Box3` bounds +* compute `center` and `size` +* set `controls.target = center` +* set camera distance so the whole bounding box fits (using FOV + max dimension) +* optionally lock azimuth/elevation (so every part is viewed from the same angle) + +This yields consistent “fill” and eliminates manual tuning. + +**B. Fixed camera (only if all parts are same scale and you want exact same viewpoint)** +Works if your GLBs are all in the same coordinate system/scale and similarly sized. + +### 3) Lighting rig + +Save: + +* light types (Directional/Hemisphere/Ambient) +* each light’s `position`, `intensity`, `color` +* shadow settings if used + +Your current screenshot shows a simple shaded render; keep lighting stable or you’ll get different highlight/shadow shapes across parts. + +### 4) Renderer and capture settings + +Save: + +* `width`, `height` +* `devicePixelRatio` you used (often set explicitly for determinism) +* `renderer.toneMapping`, `outputColorSpace` (Three.js r152+ uses `renderer.outputColorSpace = THREE.SRGBColorSpace`) +* `renderer.setClearColor()` (white) and alpha behavior +* `preserveDrawingBuffer: true` (for browser capture) +* whether you use `physicallyCorrectLights` (if you do, keep it constant) + +### 5) Material overrides / wireframe settings + +If you’re overriding materials for “clay render” or wireframe, save: + +* your override material parameters (`MeshStandardMaterial` vs `MeshPhongMaterial`, roughness/metalness) +* edge/wire settings (`EdgesGeometry` threshold angle, line material linewidth/color) + +### 6) Background / staging + +Save: + +* background color (white) +* whether you add a ground plane, axes, environment map, etc. + +Even “invisible” helpers can affect auto-framing if they’re included in bounds—so exclude them from bounds computation. + +--- + +## The best thing to save: a small “render profile” JSON + +Put this in a file like `render_profile.json`: + +* canvas size +* camera angle (azimuth/elevation) OR explicit camera position +* fit-to-frame padding factor (e.g., 1.15) +* light positions/intensities +* material mode (`source`, `clay`, `wireframe`, `edges`) +* background color + +Then your batch renderer (browser or headless) loads profile + GLB and produces PNG. + +Example shape (conceptually): + +```json +{ + "output": { "w": 1600, "h": 1200, "pixelRatio": 1 }, + "background": "#ffffff", + "camera": { "fov": 35, "near": 0.01, "far": 1000, "azimuth": -35, "elevation": 20, "fitPadding": 1.15 }, + "lights": [ + { "type": "dir", "pos": [3,5,2], "intensity": 2.8 }, + { "type": "hem", "sky": "#ffffff", "ground": "#666666", "intensity": 0.6 } + ], + "materialMode": "clay" +} +``` + +--- + +## How to mass-create PNGs (two viable routes) + +### Route 1: Headless batch (best) + +Use **node + headless WebGL** (or Puppeteer driving Chrome) to render each GLB with the profile and write PNGs. This is what you want if you’re doing hundreds/thousands. + +### Route 2: Browser batch page (simpler) + +A page that: + +* loads a list of GLB URLs +* applies the profile +* renders each, downloads PNG automatically (or zips them) + +This is simpler but less pleasant at scale. + +--- + +## One key question that determines the “right” profile + +Do you want your PNGs to be: + +* **same camera angle + fit-to-frame** (parts all fill the image similarly), or +* **same absolute camera** (so relative size differences are visible)? + +If you tell me which you want, I’ll give you a concrete “profile JSON + camera fit math + batch driver” that matches it. diff --git a/exercises/create_png_profile/Screenshot_20260304_102153_AM_Wed-1.png b/exercises/create_png_profile/Screenshot_20260304_102153_AM_Wed-1.png new file mode 100644 index 0000000..a1f35d6 Binary files /dev/null and b/exercises/create_png_profile/Screenshot_20260304_102153_AM_Wed-1.png differ diff --git a/exercises/create_png_profile/Screenshot_20260304_102435_AM_Wed.png b/exercises/create_png_profile/Screenshot_20260304_102435_AM_Wed.png new file mode 100644 index 0000000..0afdf19 Binary files /dev/null and b/exercises/create_png_profile/Screenshot_20260304_102435_AM_Wed.png differ diff --git a/exercises/create_png_profile/Screenshot_20260304_154311_PM_Wed.png b/exercises/create_png_profile/Screenshot_20260304_154311_PM_Wed.png new file mode 100644 index 0000000..23ddf12 Binary files /dev/null and b/exercises/create_png_profile/Screenshot_20260304_154311_PM_Wed.png differ diff --git a/lab.html b/lab.html deleted file mode 100644 index 63fd0f1..0000000 --- a/lab.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - -RenderLab v1 - - - - - - diff --git a/mv_themes.js b/mv_themes.js new file mode 100644 index 0000000..db13be7 --- /dev/null +++ b/mv_themes.js @@ -0,0 +1,38 @@ +// mv_theme.js +// Apply consistent colors for manifest viewing. +// +// Usage: +// +// + +const THEMES = { + primary: { rgba: [0.10, 0.45, 1.00, 1.0] }, // diagnostic blue + accent: { rgba: [0.78, 0.33, 0.10, 1.0] }, // burnt orange-ish +}; + +function applyTheme(mv) { + const key = (mv.dataset.theme || "primary").toLowerCase(); + const theme = THEMES[key] || THEMES.primary; + + // Lighting-related material choices that improve face contrast + const metallic = 0.0; + const roughness = 0.35; + + for (const mat of mv.model.materials) { + mat.pbrMetallicRoughness.setBaseColorFactor(theme.rgba); + mat.pbrMetallicRoughness.setMetallicFactor(metallic); + mat.pbrMetallicRoughness.setRoughnessFactor(roughness); + } +} + +function hook(mv) { + if (mv.model) { + applyTheme(mv); + } else { + mv.addEventListener("load", () => applyTheme(mv), { once: true }); + } +} + +for (const mv of document.querySelectorAll("model-viewer.mv")) { + hook(mv); +} diff --git a/simple_server.md b/simple_server.md new file mode 100644 index 0000000..89d7670 --- /dev/null +++ b/simple_server.md @@ -0,0 +1,6 @@ +Use this command to run a simple little server for the HTML: + + python3 -m http.server 8000 + +The directory the command above is run in is the root directory. Then + http://127.0.0.1:8000/[FILE NAME] diff --git a/web/[o]_stealthburner_LED_carrier.stl.glb b/web/[o]_stealthburner_LED_carrier.stl.glb new file mode 100644 index 0000000..2e8073a Binary files /dev/null and b/web/[o]_stealthburner_LED_carrier.stl.glb differ diff --git a/web/assets/beaker.svg b/web/assets/beaker.svg new file mode 100644 index 0000000..7ebee80 --- /dev/null +++ b/web/assets/beaker.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + diff --git a/web/assets/favicon-128.png b/web/assets/favicon-128.png new file mode 100644 index 0000000..d4e428b Binary files /dev/null and b/web/assets/favicon-128.png differ diff --git a/web/assets/favicon-16.png b/web/assets/favicon-16.png new file mode 100644 index 0000000..e487fee Binary files /dev/null and b/web/assets/favicon-16.png differ diff --git a/web/assets/favicon-256.png b/web/assets/favicon-256.png new file mode 100644 index 0000000..bcb1e3d Binary files /dev/null and b/web/assets/favicon-256.png differ diff --git a/web/assets/favicon-32.png b/web/assets/favicon-32.png new file mode 100644 index 0000000..28b615f Binary files /dev/null and b/web/assets/favicon-32.png differ diff --git a/web/assets/favicon-48.png b/web/assets/favicon-48.png new file mode 100644 index 0000000..2116ec8 Binary files /dev/null and b/web/assets/favicon-48.png differ diff --git a/web/assets/favicon-64.png b/web/assets/favicon-64.png new file mode 100644 index 0000000..7ed5add Binary files /dev/null and b/web/assets/favicon-64.png differ diff --git a/web/assets/favicon.ico b/web/assets/favicon.ico new file mode 100644 index 0000000..db66ea6 Binary files /dev/null and b/web/assets/favicon.ico differ diff --git a/web/batch_glb_png/glbs/.gitignore b/web/batch_glb_png/glbs/.gitignore new file mode 100644 index 0000000..7c9d611 --- /dev/null +++ b/web/batch_glb_png/glbs/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/web/batch_glb_png/pngs/.gitignore b/web/batch_glb_png/pngs/.gitignore new file mode 100644 index 0000000..c228bb2 --- /dev/null +++ b/web/batch_glb_png/pngs/.gitignore @@ -0,0 +1,3 @@ +* +!README.md +!.gitignore diff --git a/web/favicon.ico b/web/favicon.ico new file mode 100644 index 0000000..db66ea6 Binary files /dev/null and b/web/favicon.ico differ diff --git a/web/lab.html b/web/lab.html index e37465f..6626ae9 100644 --- a/web/lab.html +++ b/web/lab.html @@ -1,8 +1,12 @@ + -RenderLab v1 +RenderLab: PNG Profile Exporter