feat(hero): v0.1.2 — scale calibration, façade detail, export preset refactor (#28)
Merged PR #28: v0.1.2 — scale calibration, façade detail, export preset Co-authored-by: Zwitschi <zwitschi82@gmail.com>
This commit was merged in pull request #28.
This commit is contained in:
1 parent
640bff9631
commit
ba68dcfdbc
60 files changed
+1114
-291
No files matched your search
+91
-29
@@ -33,6 +33,9 @@ import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import fbx_export_preset as fbx_preset # noqa: E402
|
||||
|
||||
|
||||
# --- measured / derived ------------------------------------------------------
|
||||
FRONTAGE_W = 25.75
|
||||
@@ -50,8 +53,12 @@ FRONTAGE_Y = 0.0 # front plane sits at the building line of the block fra
|
||||
PEDESTAL_H = 1.45 # column pedestal top; entrance platform sits on it
|
||||
COLUMN_D = 0.95 # Ionic shaft diameter at the base
|
||||
CAPITAL_H = 0.95
|
||||
ENTABLATURE_H = 3.40 # band between the capitals and the motto panel
|
||||
PARAPET_H = 2.20 # motto panel and cornice above the entablature
|
||||
ENTABLATURE_H = 3.00 # band between the capitals and the motto panel
|
||||
# 3.40 -> 3.00 on 2026-11-09: the photographs put the
|
||||
# colonnade at 50-60 % of the 14.5 m elevation, and our
|
||||
# 3.40/2.20 entablature+parapet left it at 47.6 %. Thinning
|
||||
# both lifts the colonnade to 7.70 m = 53.1 %, inside the band.
|
||||
PARAPET_H = 1.80 # motto panel and cornice above the entablature (see above)
|
||||
PORTAL_W = 3.10 # each of the three door openings
|
||||
PORTAL_H = 3.40
|
||||
STEP_TREAD = 0.34
|
||||
@@ -70,7 +77,7 @@ def cube(name, x, y, z, dx, dy, dz, material=None):
|
||||
return obj
|
||||
|
||||
|
||||
def cylinder(name, x, y, z0, z1, radius, material, vertices=24):
|
||||
def cylinder(name, x, y, z0, z1, radius, material, vertices=24, smooth=True):
|
||||
bpy.ops.mesh.primitive_cylinder_add(
|
||||
vertices=vertices,
|
||||
radius=radius,
|
||||
@@ -81,6 +88,30 @@ def cylinder(name, x, y, z0, z1, radius, material, vertices=24):
|
||||
obj.name = name
|
||||
if material:
|
||||
obj.data.materials.append(material)
|
||||
if smooth:
|
||||
# Round parts must shade smoothly *around* the axis while keeping the
|
||||
# caps creased. Without this the columns read as faceted prisms, and the
|
||||
# FBX carries no smoothing groups at all (Unreal warns about it).
|
||||
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(35.0))
|
||||
return obj
|
||||
|
||||
|
||||
def disc_on_wall(name, x, y, z, radius, thickness, material=None, vertices=24):
|
||||
"""A round disc mounted vertically on a wall face.
|
||||
|
||||
`cylinder()` builds along Z, so a "medallion" made with it is a pancake lying
|
||||
flat on top of the parapet rather than a disc on its face — which is what the
|
||||
first build did (1.1 m across in Y, 0.02 m thick in Z, sitting at the coping).
|
||||
This rotates the cylinder 90 degrees about X so the axis points along Y and
|
||||
the disc faces the street.
|
||||
"""
|
||||
obj = cylinder(name, x, y, z - thickness / 2.0, z + thickness / 2.0,
|
||||
radius, material, vertices=vertices)
|
||||
obj.rotation_euler = (math.radians(90.0), 0.0, 0.0)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||
return obj
|
||||
|
||||
|
||||
@@ -132,14 +163,32 @@ def build():
|
||||
col_xs.append(x)
|
||||
cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27,
|
||||
1.24, 1.24, 0.54, stone)
|
||||
cylinder(f"Hero_Column_{i + 1}", x, -0.55, PEDESTAL_H + 0.55,
|
||||
PEDESTAL_H + 0.55 + (HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H - 0.55 - CAPITAL_H),
|
||||
# Base moulding: a taller, wider cylinder where the shaft meets the
|
||||
# pedestal. Without it the shaft starts straight off a square plinth,
|
||||
# which is not how an Ionic base reads in silhouette.
|
||||
base_z0 = PEDESTAL_H + 0.55
|
||||
base_h = 0.22
|
||||
cylinder(f"Hero_ColumnBase_{i + 1}", x, -0.55, base_z0, base_z0 + base_h,
|
||||
0.68, stone, vertices=20)
|
||||
shaft_z0 = base_z0 + base_h
|
||||
cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H
|
||||
cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0,
|
||||
COLUMN_D / 2.0, stone)
|
||||
cylinder(f"Hero_Capital_{i + 1}", x, -0.55,
|
||||
HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H,
|
||||
HEIGHT - ENTABLATURE_H - PARAPET_H, 0.62, stone, vertices=16)
|
||||
# Capital block, then the square abacus that carries the architrave.
|
||||
# The abacus takes the top 0.14 m of the capital zone so the colonnade
|
||||
# still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged).
|
||||
abacus_h = 0.14
|
||||
cylinder(f"Hero_Capital_{i + 1}", x, -0.55, cap_z0,
|
||||
HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h, 0.62, stone, vertices=16)
|
||||
cube(f"Hero_Abacus_{i + 1}", x, -0.55,
|
||||
HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h / 2.0,
|
||||
1.30, 1.30, abacus_h, stone)
|
||||
note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H),
|
||||
f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)")
|
||||
note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22),
|
||||
"assumption: Ionic base moulding; photographs show a wider base at the shaft foot")
|
||||
note("Hero_Abacus_x6", (1.30, 1.30, 0.14),
|
||||
"assumption: square abacus under the architrave, as Ionic capitals carry")
|
||||
|
||||
# --- entablature and parapet ------------------------------------------
|
||||
ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H
|
||||
@@ -147,9 +196,25 @@ def build():
|
||||
# elevation reads as layered stone rather than one flat plane.
|
||||
cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone)
|
||||
note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands")
|
||||
# Cornice under the parapet.
|
||||
cube("Hero_Cornice", 0.0, -1.6, HEIGHT - PARAPET_H - 0.18, FRONTAGE_W, 1.75, 0.36, stone)
|
||||
# Cornice under the parapet: a projecting corona carried on a dentil course,
|
||||
# so the top of the order reads as layered stone with a shadow line rather
|
||||
# than one flat box. Dentils hang under the corona and stand 0.25 m clear of
|
||||
# it, which is what gives the elevation its cornice shadow at street level.
|
||||
cornice_z = HEIGHT - PARAPET_H - 0.18
|
||||
cube("Hero_Cornice", 0.0, -1.6, cornice_z, FRONTAGE_W, 1.75, 0.36, stone)
|
||||
note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line")
|
||||
DENTIL_W, DENTIL_D, DENTIL_H, DENTIL_GAP = 0.18, 0.30, 0.22, 0.27
|
||||
dentil_y = -2.30
|
||||
dentil_z = cornice_z - 0.18 - DENTIL_H / 2.0
|
||||
dentil_span = FRONTAGE_W - 2.0
|
||||
dentil_count = int(dentil_span / (DENTIL_W + DENTIL_GAP))
|
||||
for i in range(dentil_count):
|
||||
dx = -dentil_span / 2.0 + (DENTIL_W + DENTIL_GAP) * i + DENTIL_W / 2.0
|
||||
cube(f"Hero_Dentil_{i + 1}", dx, dentil_y, dentil_z,
|
||||
DENTIL_W, DENTIL_D, DENTIL_H, stone)
|
||||
note("Hero_Dentil_xN", (DENTIL_W, DENTIL_D, DENTIL_H),
|
||||
f"{dentil_count} dentils at {DENTIL_W + DENTIL_GAP:.2f} m centres under the corona; "
|
||||
"assumption: photographed cornice reads as a dentilled course")
|
||||
cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0,
|
||||
FRONTAGE_W, 1.5, ENTABLATURE_H, stone)
|
||||
note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth")
|
||||
@@ -163,11 +228,13 @@ def build():
|
||||
FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark)
|
||||
note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade")
|
||||
|
||||
# Medallions either side of the inscription.
|
||||
# Medallions either side of the inscription: discs on the parapet face, not
|
||||
# pancakes on the coping (see disc_on_wall).
|
||||
for i, x in enumerate((-6.2, 6.2), start=1):
|
||||
cylinder(f"Hero_Medallion_{i}", x, -1.36, HEIGHT - PARAPET_H / 2.0 + 0.42,
|
||||
HEIGHT - PARAPET_H / 2.0 + 0.44, 0.55, stone_dark, vertices=20)
|
||||
note("Hero_Medallion_x2", (1.1, 1.1, 0.02), "photographs: two stone discs beside the motto")
|
||||
disc_on_wall(f"Hero_Medallion_{i}", x, -1.30, HEIGHT - PARAPET_H / 2.0,
|
||||
0.55, 0.04, stone_dark, vertices=20)
|
||||
note("Hero_Medallion_x2", (1.1, 0.04, 1.1),
|
||||
"photographs: two stone discs beside the motto, mounted on the parapet face")
|
||||
|
||||
# --- entrances ---------------------------------------------------------
|
||||
# Three portals between column pairs, each with its own short flight. The
|
||||
@@ -241,32 +308,27 @@ def build():
|
||||
|
||||
# --- windows -----------------------------------------------------------
|
||||
# Two ranks of windows between the columns and behind the colonnade.
|
||||
# Y must sit on the wall plane: the mass front face is at Y -0.50 and the
|
||||
# columns stand proud to -1.02. These were at -1.98, i.e. 1.44 m in FRONT of
|
||||
# the wall and ahead of the column line, so the glazing floated in the
|
||||
# colonnade instead of reading as openings behind it (visible in the
|
||||
# review-hero-facade-* shots). WINDOW_Y puts the pane 6 cm proud of the wall,
|
||||
# safely behind the columns.
|
||||
WINDOW_Y = -0.56
|
||||
for rank, z in enumerate((4.4, 8.2), start=1):
|
||||
for i, x in enumerate(col_xs[:-1], start=1):
|
||||
wx = x + COLUMN_BAY / 2.0
|
||||
cube(f"Hero_Window_{rank}_{i}", wx, -1.98, z, 2.0, 0.08, 1.9, glass)
|
||||
cube(f"Hero_Window_{rank}_{i}", wx, WINDOW_Y, z, 2.0, 0.08, 1.9, glass)
|
||||
note("Hero_Window_x10", (2.0, 0.08, 1.9), "photographs: window bands behind the colonnade")
|
||||
|
||||
return parts, lines
|
||||
|
||||
|
||||
def export_fbx(path):
|
||||
# Preset lives in Tools/fbx_export_preset.py (ue_metric_v1).
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py.
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=path,
|
||||
use_selection=False,
|
||||
object_types={"MESH"},
|
||||
apply_unit_scale=True,
|
||||
apply_scale_options="FBX_SCALE_NONE",
|
||||
axis_forward="-Y",
|
||||
axis_up="Z",
|
||||
use_space_transform=True,
|
||||
use_mesh_modifiers=True,
|
||||
mesh_smooth_type="OFF",
|
||||
path_mode="AUTO",
|
||||
bake_space_transform=False,
|
||||
)
|
||||
fbx_preset.export_static(path)
|
||||
print(f"[hero] exported FBX preset ue_metric_v1 -> {path}")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user