Add reusable procedural helper library
This commit is contained in:
parent
eb650ad7b5
commit
aa4d49da1a
18 changed files with 378 additions and 242 deletions
|
|
@ -49,13 +49,14 @@ Currently present:
|
|||
|
||||
- persistence surfaces for re-entry, evidence, planning, and compaction hygiene
|
||||
- starter architecture notes for the Blender scene-generation pipeline
|
||||
- a small `vibegeometry/` helper library for coordinate frames, deterministic
|
||||
fields, batching, Blender object/material/camera helpers, and verifier checks
|
||||
- a repo structure for schemas, prompts, examples, experiments, scripts, tools,
|
||||
notes, and durable state
|
||||
- small state inspection and pre-compaction helpers
|
||||
|
||||
Not present yet:
|
||||
|
||||
- Blender Python graph builder library
|
||||
- scene brief schema
|
||||
- node graph manifest schema
|
||||
- render review schema
|
||||
|
|
@ -73,6 +74,7 @@ Not present yet:
|
|||
- `notes/fresh-workspace-handoff.md`: compact re-entry packet
|
||||
- `notes/vibegeometry-current-system-map.md`: current repo machinery
|
||||
- `notes/vibegeometry-implementation-plan.md`: implementation sequence
|
||||
- `vibegeometry/`: reusable procedural-scene helper library
|
||||
- `notes/architecture-rationale.md`: why the persistence surfaces exist
|
||||
- `docs/architecture/`: pipeline contracts and design notes
|
||||
- `docs/research/`: Blender geometry-node research notes
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ This is intentionally hybrid:
|
|||
|
||||
- Python structures the lore-derived coordinate system, layer tables, material
|
||||
meanings, mapped civic regions, cameras, labels, and render paths.
|
||||
- `vibegeometry/` supplies the reusable Python primitives promoted from this
|
||||
build: coordinate frames, deterministic fields, batched mesh construction,
|
||||
Blender object/material/camera helpers, and verifier checks.
|
||||
- Geometry Script emits `VG Bloom Light Spine`, an inspectable 21-node
|
||||
geometry-node group used by a Nodes modifier in the scene.
|
||||
- bpy builds the full shell surfaces, despun core, hub cap, docking port, spun
|
||||
|
|
|
|||
|
|
@ -22,6 +22,21 @@ except ModuleNotFoundError:
|
|||
load_repo_geometry_script()
|
||||
|
||||
from geometry_script import * # noqa: F403 - Geometry Script exposes node functions as DSL globals.
|
||||
from vibegeometry import (
|
||||
CylinderFrame,
|
||||
add_box,
|
||||
add_curve_polyline,
|
||||
add_cylinder_between,
|
||||
add_material,
|
||||
add_mesh_parts,
|
||||
add_multi_polyline_curve,
|
||||
aim_camera,
|
||||
append_box_parts,
|
||||
append_cylindrical_ring_band,
|
||||
clear_scene,
|
||||
fbm_2d,
|
||||
smoothstep,
|
||||
)
|
||||
|
||||
|
||||
TAU = pymath.tau
|
||||
|
|
@ -31,6 +46,12 @@ CUT_END = pymath.radians(132.0)
|
|||
FULL_START = -pymath.pi
|
||||
FULL_END = pymath.pi
|
||||
INNER_RADIUS = 5.0
|
||||
FRAME = CylinderFrame(INNER_RADIUS)
|
||||
radial = FRAME.radial
|
||||
tangent = FRAME.tangent
|
||||
cyl_point = FRAME.cyl_point
|
||||
surface_point = FRAME.surface_point
|
||||
mat = add_material
|
||||
|
||||
|
||||
@tree("VG Bloom Light Spine")
|
||||
|
|
@ -55,42 +76,6 @@ def vg_bloom_light_spine(
|
|||
return {"Geometry": curve_to_mesh(curve=positioned, profile_curve=profile, fill_caps=True)}
|
||||
|
||||
|
||||
def mat(name, color, emission=False, strength=0.0, alpha=1.0):
|
||||
import bpy
|
||||
|
||||
material = bpy.data.materials.new(name)
|
||||
material.diffuse_color = (color[0], color[1], color[2], alpha)
|
||||
material.use_nodes = True
|
||||
bsdf = material.node_tree.nodes.get("Principled BSDF")
|
||||
if bsdf:
|
||||
if emission:
|
||||
bsdf.inputs["Emission Color"].default_value = (color[0], color[1], color[2], 1.0)
|
||||
bsdf.inputs["Emission Strength"].default_value = strength
|
||||
bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], alpha)
|
||||
bsdf.inputs["Roughness"].default_value = 0.62
|
||||
bsdf.inputs["Alpha"].default_value = alpha
|
||||
if alpha < 1.0:
|
||||
material.blend_method = "BLEND"
|
||||
material.use_screen_refraction = True
|
||||
material.show_transparent_back = True
|
||||
return material
|
||||
|
||||
|
||||
def clear_scene():
|
||||
import bpy
|
||||
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete()
|
||||
|
||||
|
||||
def radial(angle: float):
|
||||
return (0.0, pymath.cos(angle), pymath.sin(angle))
|
||||
|
||||
|
||||
def tangent(angle: float):
|
||||
return (0.0, -pymath.sin(angle), pymath.cos(angle))
|
||||
|
||||
|
||||
def add_shell_segment(name, radius, half_length, thickness, material, angle_steps=144, x_steps=8, start_angle=FULL_START, end_angle=FULL_END):
|
||||
import bpy
|
||||
|
||||
|
|
@ -100,7 +85,7 @@ def add_shell_segment(name, radius, half_length, thickness, material, angle_step
|
|||
x = -half_length + (2.0 * half_length * ix / x_steps)
|
||||
for ia in range(angle_steps + 1):
|
||||
a = start_angle + (end_angle - start_angle) * ia / angle_steps
|
||||
verts.append((x, radius * pymath.cos(a), radius * pymath.sin(a)))
|
||||
verts.append(cyl_point(x, a, radius))
|
||||
for ix in range(x_steps):
|
||||
for ia in range(angle_steps):
|
||||
row = angle_steps + 1
|
||||
|
|
@ -118,11 +103,6 @@ def add_shell_segment(name, radius, half_length, thickness, material, angle_step
|
|||
return obj
|
||||
|
||||
|
||||
def surface_point(x, angle, radius=INNER_RADIUS, lift=-0.04):
|
||||
r = radius + lift
|
||||
return (x, r * pymath.cos(angle), r * pymath.sin(angle))
|
||||
|
||||
|
||||
def add_surface_patch(name, x0, x1, a0, a1, radius, material, lift=-0.045, angle_steps=8, x_steps=3):
|
||||
import bpy
|
||||
|
||||
|
|
@ -177,68 +157,6 @@ def add_surface_block_field(name, x_values, angles, material, size, radial_lift=
|
|||
)
|
||||
|
||||
|
||||
def append_box_parts(verts, faces, loc, axes, size):
|
||||
from mathutils import Vector
|
||||
|
||||
origin = Vector(loc)
|
||||
ax = [Vector(axis).normalized() for axis in axes]
|
||||
sx, sy, sz = (value * 0.5 for value in size)
|
||||
base = len(verts)
|
||||
for dx, dy, dz in [
|
||||
(-sx, -sy, -sz),
|
||||
(sx, -sy, -sz),
|
||||
(sx, sy, -sz),
|
||||
(-sx, sy, -sz),
|
||||
(-sx, -sy, sz),
|
||||
(sx, -sy, sz),
|
||||
(sx, sy, sz),
|
||||
(-sx, sy, sz),
|
||||
]:
|
||||
v = origin + ax[0] * dx + ax[1] * dy + ax[2] * dz
|
||||
verts.append((v.x, v.y, v.z))
|
||||
faces.extend(
|
||||
[
|
||||
(base + 0, base + 1, base + 2, base + 3),
|
||||
(base + 4, base + 7, base + 6, base + 5),
|
||||
(base + 0, base + 4, base + 5, base + 1),
|
||||
(base + 1, base + 5, base + 6, base + 2),
|
||||
(base + 2, base + 6, base + 7, base + 3),
|
||||
(base + 3, base + 7, base + 4, base + 0),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def add_mesh_parts(name, verts, faces, material):
|
||||
import bpy
|
||||
|
||||
mesh = bpy.data.meshes.new(name + "Mesh")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_multi_polyline_curve(name, lines, bevel, material, resolution=2):
|
||||
import bpy
|
||||
|
||||
curve = bpy.data.curves.new(name + "Curve", "CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = resolution
|
||||
curve.bevel_depth = bevel
|
||||
curve.bevel_resolution = 2
|
||||
for line in lines:
|
||||
spline = curve.splines.new("POLY")
|
||||
spline.points.add(len(line) - 1)
|
||||
for point, co in zip(spline.points, line):
|
||||
point.co = (co[0], co[1], co[2], 1.0)
|
||||
obj = bpy.data.objects.new(name, curve)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_surface_stilt(name, x, angle, inner_lift, outer_lift, material, radius=0.009):
|
||||
start = surface_point(x, angle, INNER_RADIUS, lift=inner_lift)
|
||||
end = surface_point(x, angle, INNER_RADIUS, lift=outer_lift)
|
||||
|
|
@ -317,17 +235,6 @@ def add_favela_fractal_cluster(prefix, x_values, angles, mats):
|
|||
add_multi_polyline_curve(prefix + "_brace_and_catwalks", brace_lines, 0.005, mats["brace"], resolution=2)
|
||||
|
||||
|
||||
def append_endcap_ring_band(verts, faces, x, inner_radius, outer_radius, segments=96):
|
||||
base = len(verts)
|
||||
for i in range(segments):
|
||||
a = FULL_START + TAU * i / segments
|
||||
verts.append(cyl_point(x, a, inner_radius))
|
||||
verts.append(cyl_point(x, a, outer_radius))
|
||||
for i in range(segments):
|
||||
j = (i + 1) % segments
|
||||
faces.append((base + i * 2, base + j * 2, base + j * 2 + 1, base + i * 2 + 1))
|
||||
|
||||
|
||||
def add_hubward_endcap_terraced_slums(prefix, x, mats):
|
||||
# Bloom lore frame: on an endcap, "up" is inward toward the axial Spire.
|
||||
# These terraces are not polar graph paper. Fractal noise makes the shelf
|
||||
|
|
@ -345,7 +252,7 @@ def add_hubward_endcap_terraced_slums(prefix, x, mats):
|
|||
outer_r = rings[tier + 1]
|
||||
tier_t = tier / (len(rings) - 2)
|
||||
face_x = x + 0.035 + tier * 0.018
|
||||
append_endcap_ring_band(shelf_verts, shelf_faces, face_x, inner_r, outer_r, segments=160)
|
||||
append_cylindrical_ring_band(shelf_verts, shelf_faces, face_x, inner_r, outer_r, segments=160)
|
||||
count = int(30 + tier * 8 + 18 * fbm_2d(tier * 0.71, 4.0, seed=12))
|
||||
angular_step = TAU / count
|
||||
run = 0
|
||||
|
|
@ -414,7 +321,7 @@ def add_hubward_endcap_terraced_slums(prefix, x, mats):
|
|||
def add_hubward_office_complex(prefix, x, mats):
|
||||
office_verts, office_faces = [], []
|
||||
for i, radius in enumerate((0.42, 0.68, 0.92)):
|
||||
append_endcap_ring_band(office_verts, office_faces, x + 0.18 + i * 0.06, radius, radius + 0.16, segments=96)
|
||||
append_cylindrical_ring_band(office_verts, office_faces, x + 0.18 + i * 0.06, radius, radius + 0.16, segments=96)
|
||||
add_mesh_parts(prefix + "_ring_office_mesh", office_verts, office_faces, mats["hub"])
|
||||
for i, angle in enumerate([pymath.radians(a) for a in range(-150, 181, 30)]):
|
||||
add_cylinder_between(
|
||||
|
|
@ -511,48 +418,6 @@ def add_frame_transfer_artery(name, x, angle, start_radius, end_radius, material
|
|||
return add_curve_polyline(name, samples, bevel, material)
|
||||
|
||||
|
||||
def cyl_point(x, angle, radius):
|
||||
return (x, radius * pymath.cos(angle), radius * pymath.sin(angle))
|
||||
|
||||
|
||||
def smoothstep(t):
|
||||
return t * t * (3.0 - 2.0 * t)
|
||||
|
||||
|
||||
def hash01(ix, iy=0, seed=0):
|
||||
n = ix * 374761393 + iy * 668265263 + seed * 1442695041
|
||||
n = (n ^ (n >> 13)) * 1274126177
|
||||
n = n ^ (n >> 16)
|
||||
return (n & 0xFFFFFFFF) / 0xFFFFFFFF
|
||||
|
||||
|
||||
def value_noise_2d(x, y, seed=0):
|
||||
ix = pymath.floor(x)
|
||||
iy = pymath.floor(y)
|
||||
fx = smoothstep(x - ix)
|
||||
fy = smoothstep(y - iy)
|
||||
a = hash01(ix, iy, seed)
|
||||
b = hash01(ix + 1, iy, seed)
|
||||
c = hash01(ix, iy + 1, seed)
|
||||
d = hash01(ix + 1, iy + 1, seed)
|
||||
ab = a + (b - a) * fx
|
||||
cd = c + (d - c) * fx
|
||||
return ab + (cd - ab) * fy
|
||||
|
||||
|
||||
def fbm_2d(x, y, seed=0, octaves=4):
|
||||
value = 0.0
|
||||
amplitude = 0.5
|
||||
frequency = 1.0
|
||||
total = 0.0
|
||||
for octave in range(octaves):
|
||||
value += value_noise_2d(x * frequency, y * frequency, seed + octave * 19) * amplitude
|
||||
total += amplitude
|
||||
amplitude *= 0.5
|
||||
frequency *= 2.03
|
||||
return value / total if total else 0.0
|
||||
|
||||
|
||||
def add_spiral_spoke_loop(name, x, angle, start_radius, end_radius, material, bevel=0.04, cargo=False):
|
||||
"""Shared cylinder frame: x is axial, angle is shell azimuth, radius is spin radius."""
|
||||
samples = []
|
||||
|
|
@ -615,60 +480,6 @@ def add_endcap_terrace_blocks(prefix, x, radii, angles, material, height=0.18):
|
|||
)
|
||||
|
||||
|
||||
def add_cylinder_between(name, start, end, radius, material, vertices=16):
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
start_v = Vector(start)
|
||||
end_v = Vector(end)
|
||||
mid = (start_v + end_v) / 2.0
|
||||
direction = end_v - start_v
|
||||
length = direction.length
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=length, location=mid)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.name = name + "Mesh"
|
||||
obj.rotation_euler = direction.to_track_quat("Z", "Y").to_euler()
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_curve_polyline(name, points, bevel, material, resolution=2):
|
||||
import bpy
|
||||
|
||||
curve = bpy.data.curves.new(name + "Curve", "CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = resolution
|
||||
curve.bevel_depth = bevel
|
||||
curve.bevel_resolution = 4
|
||||
spline = curve.splines.new("POLY")
|
||||
spline.points.add(len(points) - 1)
|
||||
for point, co in zip(spline.points, points):
|
||||
point.co = (co[0], co[1], co[2], 1.0)
|
||||
obj = bpy.data.objects.new(name, curve)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_box(name, loc, axes, size, material):
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.dimensions = size
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix.col[0].xyz = Vector(axes[0]).normalized()
|
||||
matrix.col[1].xyz = Vector(axes[1]).normalized()
|
||||
matrix.col[2].xyz = Vector(axes[2]).normalized()
|
||||
obj.rotation_euler = matrix.to_euler()
|
||||
obj.data.materials.append(material)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
return obj
|
||||
|
||||
|
||||
def add_label(name, text, loc, size, material):
|
||||
import bpy
|
||||
|
||||
|
|
@ -696,13 +507,6 @@ def add_gn_light_spine(material):
|
|||
return obj
|
||||
|
||||
|
||||
def aim_camera(obj, target):
|
||||
from mathutils import Vector
|
||||
|
||||
direction = Vector(target) - obj.location
|
||||
obj.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||
|
||||
|
||||
def build_scene():
|
||||
import bpy
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2 MiB After Width: | Height: | Size: 2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
|
@ -63,6 +63,11 @@ tables, loops, variants, cleanup, validation, docs, and scene/render
|
|||
orchestration; Geometry Script emits inspectable Geometry Nodes groups; `bpy`
|
||||
handles Blender scene machinery and evaluated evidence.
|
||||
|
||||
`vibegeometry/` is the first reusable helper library. It currently holds
|
||||
coordinate-frame helpers, deterministic field/noise helpers, mesh batching,
|
||||
thin Blender object/material/camera helpers, and verifier helpers. The Bloom
|
||||
script imports these primitives directly, so the library is not just a shrine.
|
||||
|
||||
The repo-local Geometry Script clone has Blender 5.1 work for nested `@tree`
|
||||
group reuse, a ported `nodes_to_script` converter, and menu-socket default
|
||||
tolerance. See `docs/research/geometry-script-fork-notes.md`. Run
|
||||
|
|
@ -77,9 +82,9 @@ The GameCult fork is intentionally pruned to `main` plus VibeGeometry-owned
|
|||
tooling branches. Do not copy upstream branch soup forward unless a future patch
|
||||
actually needs it.
|
||||
|
||||
The current next action is to extract the Bloom coordinate/noise/detail lessons
|
||||
into a small reusable builder seam, starting with either endcap terrace slums or
|
||||
spiral transfer arteries, then bind that seam to scene-brief, manifest,
|
||||
The current next action is to grow `vibegeometry/` from helper primitives into a
|
||||
small reusable builder seam, starting with either endcap terrace slums or spiral
|
||||
transfer arteries, then bind that seam to scene-brief, manifest,
|
||||
inspection-render, and verifier contract drafts.
|
||||
|
||||
## Important Invariants
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ not carry live next-action clutter.
|
|||
## Near-Term Sequence
|
||||
|
||||
1. Promote the Bloom coordinate/noise lessons into reusable builder seams.
|
||||
- Extract shared cylindrical/endcap coordinate helpers into a clear module or
|
||||
Geometry Script-oriented builder layer.
|
||||
- Build on the new `vibegeometry/` helper library rather than adding more
|
||||
one-off example functions.
|
||||
- Preserve the split between coordinate contract, density field, batched
|
||||
realization, domain camera, and verifier checks.
|
||||
- Keep the first extraction small: endcap terrace slums or spiral transfer
|
||||
|
|
|
|||
|
|
@ -41,3 +41,4 @@
|
|||
{"ts": "2026-05-05T00:33:45+00:00", "type": "correction", "status": "accepted", "note": "Corrected Bloom terrace-slum orientation against lore: on hubward endcaps, up is inward toward the Spire, so slums are annular shelves around the docking hub office complex growing outward to the surface; removed the misleading cylinder-wall favela framing and verifier now checks hubward_endcap_rice_paddy_slums objects."}
|
||||
{"ts": "2026-05-05T00:55:36+00:00", "type": "correction", "status": "accepted", "note": "Bloom endcap terrace slums now use fractal value-noise occupancy rather than neat concentric box rings; verified dedicated hubward-endcap render and mesh-density checks for poor shacks, midrise housing, and outer urban crown."}
|
||||
{"ts": "2026-05-05T01:12:46+00:00", "type": "maintenance", "status": "accepted", "note": "Polished persistent state after Bloom corrections: map, handoff, plan, and doctrine now prioritize source coordinate contracts, deterministic noise for organic occupancy, batched dense detail, domain-facing inspection renders, and verifier checks as reusable machine behavior rather than one-off scene scars."}
|
||||
{"ts": "2026-05-05T01:25:43+00:00", "type": "implementation", "status": "accepted", "note": "Created the first reusable vibegeometry Python helper library for coordinate frames, deterministic fields, dense mesh batching, Blender scene primitives, and verifier helpers; refactored the Bloom build and verifier to use it, then rebuilt and verified the scene in Blender with 1028 geometry/text objects."}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ live_architecture:
|
|||
- Review renders against the visual target and record accepted, rejected, or revised hypotheses.
|
||||
authoring_stack:
|
||||
- Python structures intent, repetition, data tables, variants, cleanup, validation, documentation, and scene/render orchestration.
|
||||
- `vibegeometry/` provides reusable Python helpers for coordinate contracts, deterministic fields, batched geometry, Blender object/material/camera setup, and verifier checks.
|
||||
- Geometry Script emits inspectable reusable Geometry Nodes groups.
|
||||
- bpy handles Blender objects, modifiers, materials, cameras, lights, files, renders, and evaluated-geometry checks.
|
||||
- External Python tools inspect source blends, generate converter drafts, compare outputs, and maintain corpus evidence.
|
||||
|
|
@ -73,6 +74,7 @@ current_artifacts:
|
|||
- docs/research/translations/ircss-french-houses-node-atlas.md
|
||||
- docs/research/translations/ircss-french-houses-node-mechanisms.md
|
||||
- docs/research/builds/aetheria-bloom-habitat.md
|
||||
- vibegeometry/
|
||||
tools:
|
||||
- tools/vibegeometry_state.py
|
||||
- tools/vibegeometry_prepare_compaction.py
|
||||
|
|
@ -97,8 +99,8 @@ data_flow:
|
|||
- If state grows faster than understanding, cut it back.
|
||||
|
||||
current_status:
|
||||
summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus a from-scratch Aetheria Bloom whole-habitat build. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; organic occupancy needs deterministic noise fields; dense detail must be batched; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. Bloom currently demonstrates shared cylindrical/endcap helpers, golden-angle transfer loops, layered shell/endcaps, noise-driven endcap terrace slums, social-gradient geography, roads/rivers/clouds, and service markers. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes.
|
||||
next_action: Extract the Bloom coordinate/noise/detail lessons into a small reusable builder seam, starting with either endcap terrace slums or spiral transfer arteries, then bind that seam to scene-brief, manifest, inspection-render, and verifier contract drafts.
|
||||
summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus a from-scratch Aetheria Bloom whole-habitat build. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; organic occupancy needs deterministic noise fields; dense detail must be batched; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom build. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes.
|
||||
next_action: Grow `vibegeometry/` from helper primitives into builder seams by extracting one higher-level pattern next: either endcap terrace slums or spiral transfer arteries, then bind that seam to scene-brief, manifest, inspection-render, and verifier contract drafts.
|
||||
remotes:
|
||||
vibegeometry: https://github.com/GameCult/VibeGeometry
|
||||
geometry_script_fork: https://github.com/GameCult/geometry-script
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from vibegeometry import assert_objects_exist, assert_render_artifacts, assert_scene_density
|
||||
|
||||
ARTIFACT_DIR = ROOT / "experiments" / "generated" / "aetheria_bloom"
|
||||
|
||||
|
||||
|
|
@ -83,8 +88,7 @@ def assert_true(condition: bool, message: str):
|
|||
|
||||
|
||||
def verify_scene():
|
||||
missing = sorted(name for name in REQUIRED_OBJECTS if name not in bpy.data.objects)
|
||||
assert_true(not missing, f"Missing required objects: {missing}")
|
||||
assert_objects_exist(bpy, REQUIRED_OBJECTS)
|
||||
|
||||
group = bpy.data.node_groups.get("VG Bloom Light Spine")
|
||||
assert_true(group is not None, "Missing Geometry Script node group VG Bloom Light Spine")
|
||||
|
|
@ -94,18 +98,17 @@ def verify_scene():
|
|||
carrier = bpy.data.objects["GN_Light_Spine_Modifier_Carrier"]
|
||||
assert_true(any(mod.type == "NODES" and mod.node_group == group for mod in carrier.modifiers), "Light-spine carrier lacks expected geometry-nodes modifier")
|
||||
|
||||
for filename in (
|
||||
"aetheria_bloom_habitat.png",
|
||||
"aetheria_bloom_interior_world.png",
|
||||
"aetheria_bloom_hubward_endcap_terraces.png",
|
||||
"aetheria_bloom_habitat.blend",
|
||||
):
|
||||
path = ARTIFACT_DIR / filename
|
||||
assert_true(path.exists(), f"Missing generated artifact {path}")
|
||||
assert_true(path.stat().st_size > 10_000, f"Generated artifact is suspiciously small: {path}")
|
||||
assert_render_artifacts(
|
||||
ARTIFACT_DIR / filename
|
||||
for filename in (
|
||||
"aetheria_bloom_habitat.png",
|
||||
"aetheria_bloom_interior_world.png",
|
||||
"aetheria_bloom_hubward_endcap_terraces.png",
|
||||
"aetheria_bloom_habitat.blend",
|
||||
)
|
||||
)
|
||||
|
||||
mesh_objects = [obj for obj in bpy.data.objects if obj.type in {"MESH", "CURVE", "FONT"}]
|
||||
assert_true(len(mesh_objects) >= 930, f"Expected a populated whole-Bloom study scene, found {len(mesh_objects)} geometry/text objects")
|
||||
mesh_objects = assert_scene_density(bpy, 930)
|
||||
|
||||
slum_mesh = bpy.data.objects["hubward_endcap_rice_paddy_slums_rickety_shack_mesh"].data
|
||||
mid_mesh = bpy.data.objects["hubward_endcap_rice_paddy_slums_midrise_housing_mesh"].data
|
||||
|
|
|
|||
37
vibegeometry/__init__.py
Normal file
37
vibegeometry/__init__.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Reusable procedural-scene helpers for VibeGeometry examples."""
|
||||
|
||||
from .batching import append_box_parts, append_cylindrical_ring_band
|
||||
from .blender import (
|
||||
add_box,
|
||||
add_curve_polyline,
|
||||
add_cylinder_between,
|
||||
add_material,
|
||||
add_mesh_parts,
|
||||
add_multi_polyline_curve,
|
||||
aim_camera,
|
||||
clear_scene,
|
||||
)
|
||||
from .coordinates import CylinderFrame
|
||||
from .fields import fbm_2d, hash01, smoothstep, value_noise_2d
|
||||
from .verify import assert_objects_exist, assert_render_artifacts, assert_scene_density
|
||||
|
||||
__all__ = [
|
||||
"CylinderFrame",
|
||||
"add_box",
|
||||
"add_curve_polyline",
|
||||
"add_cylinder_between",
|
||||
"add_material",
|
||||
"add_mesh_parts",
|
||||
"add_multi_polyline_curve",
|
||||
"aim_camera",
|
||||
"append_box_parts",
|
||||
"append_cylindrical_ring_band",
|
||||
"assert_objects_exist",
|
||||
"assert_render_artifacts",
|
||||
"assert_scene_density",
|
||||
"clear_scene",
|
||||
"fbm_2d",
|
||||
"hash01",
|
||||
"smoothstep",
|
||||
"value_noise_2d",
|
||||
]
|
||||
51
vibegeometry/batching.py
Normal file
51
vibegeometry/batching.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Mesh batching helpers for dense procedural detail."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
|
||||
def append_box_parts(verts, faces, loc, axes, size):
|
||||
"""Append an oriented box to shared mesh arrays."""
|
||||
from mathutils import Vector
|
||||
|
||||
origin = Vector(loc)
|
||||
ax = [Vector(axis).normalized() for axis in axes]
|
||||
sx, sy, sz = (value * 0.5 for value in size)
|
||||
base = len(verts)
|
||||
for dx, dy, dz in [
|
||||
(-sx, -sy, -sz),
|
||||
(sx, -sy, -sz),
|
||||
(sx, sy, -sz),
|
||||
(-sx, sy, -sz),
|
||||
(-sx, -sy, sz),
|
||||
(sx, -sy, sz),
|
||||
(sx, sy, sz),
|
||||
(-sx, sy, sz),
|
||||
]:
|
||||
v = origin + ax[0] * dx + ax[1] * dy + ax[2] * dz
|
||||
verts.append((v.x, v.y, v.z))
|
||||
faces.extend(
|
||||
[
|
||||
(base + 0, base + 1, base + 2, base + 3),
|
||||
(base + 4, base + 7, base + 6, base + 5),
|
||||
(base + 0, base + 4, base + 5, base + 1),
|
||||
(base + 1, base + 5, base + 6, base + 2),
|
||||
(base + 2, base + 6, base + 7, base + 3),
|
||||
(base + 3, base + 7, base + 4, base + 0),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def append_cylindrical_ring_band(verts, faces, x, inner_radius, outer_radius, segments=96, start_angle=-math.pi, end_angle=math.pi):
|
||||
"""Append an annular band in the Y/Z plane at axial coordinate x."""
|
||||
span = end_angle - start_angle
|
||||
base = len(verts)
|
||||
for i in range(segments):
|
||||
a = start_angle + span * i / segments
|
||||
verts.append((x, inner_radius * math.cos(a), inner_radius * math.sin(a)))
|
||||
verts.append((x, outer_radius * math.cos(a), outer_radius * math.sin(a)))
|
||||
for i in range(segments):
|
||||
j = (i + 1) % segments
|
||||
faces.append((base + i * 2, base + j * 2, base + j * 2 + 1, base + i * 2 + 1))
|
||||
|
||||
124
vibegeometry/blender.py
Normal file
124
vibegeometry/blender.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"""Thin bpy helpers for scene construction and inspection renders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def add_material(name, color, emission=False, strength=0.0, alpha=1.0):
|
||||
import bpy
|
||||
|
||||
material = bpy.data.materials.new(name)
|
||||
material.diffuse_color = (color[0], color[1], color[2], alpha)
|
||||
material.use_nodes = True
|
||||
bsdf = material.node_tree.nodes.get("Principled BSDF")
|
||||
if bsdf:
|
||||
if emission:
|
||||
bsdf.inputs["Emission Color"].default_value = (color[0], color[1], color[2], 1.0)
|
||||
bsdf.inputs["Emission Strength"].default_value = strength
|
||||
bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], alpha)
|
||||
bsdf.inputs["Roughness"].default_value = 0.62
|
||||
bsdf.inputs["Alpha"].default_value = alpha
|
||||
if alpha < 1.0:
|
||||
material.blend_method = "BLEND"
|
||||
material.use_screen_refraction = True
|
||||
material.show_transparent_back = True
|
||||
return material
|
||||
|
||||
|
||||
def clear_scene():
|
||||
import bpy
|
||||
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete()
|
||||
|
||||
|
||||
def add_mesh_parts(name, verts, faces, material):
|
||||
import bpy
|
||||
|
||||
mesh = bpy.data.meshes.new(name + "Mesh")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_multi_polyline_curve(name, lines, bevel, material, resolution=2):
|
||||
import bpy
|
||||
|
||||
curve = bpy.data.curves.new(name + "Curve", "CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = resolution
|
||||
curve.bevel_depth = bevel
|
||||
curve.bevel_resolution = 2
|
||||
for line in lines:
|
||||
spline = curve.splines.new("POLY")
|
||||
spline.points.add(len(line) - 1)
|
||||
for point, co in zip(spline.points, line):
|
||||
point.co = (co[0], co[1], co[2], 1.0)
|
||||
obj = bpy.data.objects.new(name, curve)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_cylinder_between(name, start, end, radius, material, vertices=16):
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
start_v = Vector(start)
|
||||
end_v = Vector(end)
|
||||
mid = (start_v + end_v) / 2.0
|
||||
direction = end_v - start_v
|
||||
length = direction.length
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=length, location=mid)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.data.name = name + "Mesh"
|
||||
obj.rotation_euler = direction.to_track_quat("Z", "Y").to_euler()
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_curve_polyline(name, points, bevel, material, resolution=2):
|
||||
import bpy
|
||||
|
||||
curve = bpy.data.curves.new(name + "Curve", "CURVE")
|
||||
curve.dimensions = "3D"
|
||||
curve.resolution_u = resolution
|
||||
curve.bevel_depth = bevel
|
||||
curve.bevel_resolution = 4
|
||||
spline = curve.splines.new("POLY")
|
||||
spline.points.add(len(points) - 1)
|
||||
for point, co in zip(spline.points, points):
|
||||
point.co = (co[0], co[1], co[2], 1.0)
|
||||
obj = bpy.data.objects.new(name, curve)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def add_box(name, loc, axes, size, material):
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc)
|
||||
obj = bpy.context.object
|
||||
obj.name = name
|
||||
obj.dimensions = size
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix.col[0].xyz = Vector(axes[0]).normalized()
|
||||
matrix.col[1].xyz = Vector(axes[1]).normalized()
|
||||
matrix.col[2].xyz = Vector(axes[2]).normalized()
|
||||
obj.rotation_euler = matrix.to_euler()
|
||||
obj.data.materials.append(material)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
return obj
|
||||
|
||||
|
||||
def aim_camera(obj, target):
|
||||
from mathutils import Vector
|
||||
|
||||
direction = Vector(target) - obj.location
|
||||
obj.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler()
|
||||
|
||||
33
vibegeometry/coordinates.py
Normal file
33
vibegeometry/coordinates.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Coordinate-frame helpers for procedural Blender scenes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
TAU = math.tau
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CylinderFrame:
|
||||
"""Shared cylindrical frame: x is axial, angle is azimuth, radius is spin radius."""
|
||||
|
||||
inner_radius: float
|
||||
|
||||
@staticmethod
|
||||
def radial(angle: float) -> tuple[float, float, float]:
|
||||
return (0.0, math.cos(angle), math.sin(angle))
|
||||
|
||||
@staticmethod
|
||||
def tangent(angle: float) -> tuple[float, float, float]:
|
||||
return (0.0, -math.sin(angle), math.cos(angle))
|
||||
|
||||
@staticmethod
|
||||
def cyl_point(x: float, angle: float, radius: float) -> tuple[float, float, float]:
|
||||
return (x, radius * math.cos(angle), radius * math.sin(angle))
|
||||
|
||||
def surface_point(self, x: float, angle: float, radius: float | None = None, lift: float = -0.04) -> tuple[float, float, float]:
|
||||
r = (self.inner_radius if radius is None else radius) + lift
|
||||
return self.cyl_point(x, angle, r)
|
||||
|
||||
44
vibegeometry/fields.py
Normal file
44
vibegeometry/fields.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""Deterministic scalar fields for procedural distribution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
|
||||
def smoothstep(t: float) -> float:
|
||||
return t * t * (3.0 - 2.0 * t)
|
||||
|
||||
|
||||
def hash01(ix: int, iy: int = 0, seed: int = 0) -> float:
|
||||
n = ix * 374761393 + iy * 668265263 + seed * 1442695041
|
||||
n = (n ^ (n >> 13)) * 1274126177
|
||||
n = n ^ (n >> 16)
|
||||
return (n & 0xFFFFFFFF) / 0xFFFFFFFF
|
||||
|
||||
|
||||
def value_noise_2d(x: float, y: float, seed: int = 0) -> float:
|
||||
ix = math.floor(x)
|
||||
iy = math.floor(y)
|
||||
fx = smoothstep(x - ix)
|
||||
fy = smoothstep(y - iy)
|
||||
a = hash01(ix, iy, seed)
|
||||
b = hash01(ix + 1, iy, seed)
|
||||
c = hash01(ix, iy + 1, seed)
|
||||
d = hash01(ix + 1, iy + 1, seed)
|
||||
ab = a + (b - a) * fx
|
||||
cd = c + (d - c) * fx
|
||||
return ab + (cd - ab) * fy
|
||||
|
||||
|
||||
def fbm_2d(x: float, y: float, seed: int = 0, octaves: int = 4) -> float:
|
||||
value = 0.0
|
||||
amplitude = 0.5
|
||||
frequency = 1.0
|
||||
total = 0.0
|
||||
for octave in range(octaves):
|
||||
value += value_noise_2d(x * frequency, y * frequency, seed + octave * 19) * amplitude
|
||||
total += amplitude
|
||||
amplitude *= 0.5
|
||||
frequency *= 2.03
|
||||
return value / total if total else 0.0
|
||||
|
||||
27
vibegeometry/verify.py
Normal file
27
vibegeometry/verify.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""Verification helpers for generated Blender scenes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def assert_objects_exist(bpy_module, names):
|
||||
missing = sorted(name for name in names if name not in bpy_module.data.objects)
|
||||
if missing:
|
||||
raise AssertionError(f"Missing required objects: {missing}")
|
||||
|
||||
|
||||
def assert_render_artifacts(paths, min_size=10_000):
|
||||
for path in paths:
|
||||
artifact = Path(path)
|
||||
if not artifact.exists():
|
||||
raise AssertionError(f"Missing generated artifact {artifact}")
|
||||
if artifact.stat().st_size <= min_size:
|
||||
raise AssertionError(f"Generated artifact is suspiciously small: {artifact}")
|
||||
|
||||
|
||||
def assert_scene_density(bpy_module, min_geometry_objects):
|
||||
mesh_objects = [obj for obj in bpy_module.data.objects if obj.type in {"MESH", "CURVE", "FONT"}]
|
||||
if len(mesh_objects) < min_geometry_objects:
|
||||
raise AssertionError(f"Expected a populated scene, found {len(mesh_objects)} geometry/text objects")
|
||||
return mesh_objects
|
||||
Loading…
Reference in a new issue