Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
126 lines
5.7 KiB
Python
126 lines
5.7 KiB
Python
"""Create the atlas's own solid mesh. Run with Blender --background --python.
|
|
|
|
Coordinates below are metres in Enfusion's X/right, Y/front, Z/up frame.
|
|
The printable face retains the existing 744x1024 layout and its left gutter.
|
|
The rigid book and separate turning sheet use the native atlas hand graph.
|
|
"""
|
|
from pathlib import Path
|
|
import math
|
|
import bpy
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OUT = ROOT / 'addons/core/Assets/Items/ERR_Atlas'
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete(use_global=False)
|
|
|
|
def xyz(p):
|
|
# Place the rigid paper just in front of the stock folded-map grip.
|
|
return (p[0] + .015, -p[2], p[1] + .018)
|
|
|
|
materials = {}
|
|
for name, color, metal in [('AtlasPrint', (.85,.8,.7,1), 0),
|
|
('AtlasPaper', (.73,.70,.62,1), 0),
|
|
('AtlasCover', (.48,.018,.012,1), 0),
|
|
('AtlasWire', (.8,.8,.78,1), .65)]:
|
|
mat = bpy.data.materials.new(name)
|
|
mat.diffuse_color = color
|
|
mat.use_nodes = True
|
|
bsdf = mat.node_tree.nodes.get('Principled BSDF')
|
|
bsdf.inputs['Base Color'].default_value = color
|
|
bsdf.inputs['Metallic'].default_value = metal
|
|
bsdf.inputs['Roughness'].default_value = .4 if metal else .8
|
|
materials[name] = mat
|
|
|
|
def mesh(name, vertices, faces, material, uvs=None):
|
|
data = bpy.data.meshes.new(name)
|
|
data.from_pydata([xyz(v) for v in vertices], [], faces)
|
|
data.update()
|
|
obj = bpy.data.objects.new(name, data)
|
|
bpy.context.collection.objects.link(obj)
|
|
data.materials.append(materials[material])
|
|
uv = data.uv_layers.new(name='UVMap')
|
|
for polygon in data.polygons:
|
|
for loop in polygon.loop_indices:
|
|
v = data.loops[loop].vertex_index
|
|
uv.data[loop].uv = uvs[v] if uvs else (0,0)
|
|
return obj
|
|
|
|
def box(name, lo, hi, material):
|
|
x,y,z = lo; X,Y,Z = hi
|
|
return mesh(name, [(x,y,z),(X,y,z),(X,Y,z),(x,Y,z),
|
|
(x,y,Z),(X,y,Z),(X,Y,Z),(x,Y,Z)],
|
|
[(0,3,2,1),(4,5,6,7),(0,1,5,4),(3,7,6,2),
|
|
(0,4,7,3),(1,2,6,5)], material)
|
|
|
|
# The paper mesh used a folded sheet. This replacement has separate covers,
|
|
# a 10 mm block of leaves and actual wire geometry with depth and silhouette.
|
|
left, right, bottom, top = -.083, .108, -.1485, .1485
|
|
box('BackCover', (left-.002,-.012,bottom-.002), (right+.002,-.010,top+.002), 'AtlasCover')
|
|
box('PageBlock', (left,-.010,bottom), (right,-.0005,top), 'AtlasPaper')
|
|
# Small real edge seams catch light at grazing angles without one mesh per leaf.
|
|
for n in range(12):
|
|
y = -.0096+n*.00075
|
|
box('PageEdge_%02d'%n, (left,y,bottom-.00025), (right+.0003,y+.00016,top+.00025), 'AtlasPaper')
|
|
box('FrontCover', (left-.002,-.0005,bottom-.002), (right+.002,.0005,top+.002), 'AtlasCover')
|
|
mesh('PrintedSheet', [(left,.0006,bottom),(right,.0006,bottom),
|
|
(right,.0006,top),(left,.0006,top)], [(0,3,2,1)], 'AtlasPrint',
|
|
[(80/744,1),(1,1),(1,0),(80/744,0)])
|
|
|
|
# Continuous helix around the spine; the upper arc passes through the gutter
|
|
# and the lower arc wraps behind the page block. No painted fake rings.
|
|
vertices=[]; faces=[]
|
|
turns=14; steps=turns*32; sides=6
|
|
for i in range(steps+1):
|
|
angle=i/32*2*math.pi
|
|
z=bottom+.009+(top-bottom-.018)*i/steps
|
|
for j in range(sides):
|
|
tube=j/sides*2*math.pi
|
|
radius=.0088+.0009*math.cos(tube)
|
|
vertices.append((left+.002+radius*math.cos(angle),
|
|
-.005+radius*math.sin(angle), z+.0009*math.sin(tube)))
|
|
for i in range(steps):
|
|
for j in range(sides):
|
|
a=i*sides+j; b=i*sides+(j+1)%sides
|
|
faces.append((a,b,b+sides,a+sides))
|
|
wire=mesh('SpiralWire',vertices,faces,'AtlasWire')
|
|
for poly in wire.data.polygons: poly.use_smooth=True
|
|
|
|
# A simple inventory collider; no dynamic page physics or runtime allocations.
|
|
collider=box('UBX_Atlas', (left,-.013,bottom), (right,.004,top), 'AtlasPaper')
|
|
collider['usage']='Geometry'
|
|
collider.hide_render=True
|
|
bpy.context.scene.unit_settings.system='METRIC'
|
|
bpy.context.scene.unit_settings.scale_length=1
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
source = ROOT / 'source/atlas'
|
|
source.mkdir(parents=True, exist_ok=True)
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(source/'ERR_Atlas.blend'))
|
|
bpy.ops.export_scene.fbx(filepath=str(OUT/'ERR_Atlas.fbx'), use_selection=True,
|
|
object_types={'MESH'}, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL',
|
|
axis_forward='-Z', axis_up='Y', bake_space_transform=True,
|
|
use_mesh_modifiers=True, use_custom_props=True, add_leaf_bones=False, bake_anim=False)
|
|
print('ERR ATLAS MODEL EXPORTED', OUT)
|
|
|
|
# Separate lightweight turning leaf. Its origin is the spine hinge; runtime
|
|
# rotates this visual only, keeping the held book and gripping hand steady.
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete(use_global=False)
|
|
verts=[]; faces=[]; uvs=[]
|
|
for i in range(17):
|
|
u=i/16
|
|
for z,v in [(bottom,1),(top,0)]:
|
|
# Subtract the book placement offset: this mesh is hinge-local.
|
|
verts.append((.191*u-.015, .004*math.sin(math.pi*u)-.018, z))
|
|
uvs.append((80/744+(1-80/744)*u,v))
|
|
for i in range(16):
|
|
faces.append((2*i,2*i+1,2*i+3,2*i+2))
|
|
leaf=mesh('TurningLeaf',verts,faces,'AtlasPrint',uvs)
|
|
back=mesh('LeafBack',verts,[tuple(reversed(f)) for f in faces],'AtlasPaper',uvs)
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(source/'ERR_AtlasLeaf.blend'))
|
|
bpy.ops.export_scene.fbx(filepath=str(OUT/'ERR_AtlasLeaf.fbx'),use_selection=True,
|
|
object_types={'MESH'},apply_unit_scale=True,apply_scale_options='FBX_SCALE_ALL',
|
|
axis_forward='-Z',axis_up='Y',bake_space_transform=True,
|
|
use_mesh_modifiers=True,use_custom_props=True,add_leaf_bones=False,bake_anim=False)
|