We are building frontierfoundation models for 3D.Building frontierfoundation modelsfor 3D.
From gaming to manufacturing, the 3D of the next decade will be built by models. We build foundation models and publish our research in the open.
// Use cases
Every part of an object,written as a program.
A product built in 3D, part by part, for the viewer of its product page.
The product's blocking first, then the packshot rendered on top of it.
A scene rebuilt in 3D from its point cloud, part by part.
A scene blocked out in 3D, then the shots generated on top of it.
A prop built part by part, with as many variants as the scene needs.
"""Fauteuil de bureau ergonomique, construit pièce par pièce.
blender -b -P chaise.py -- <sortie_dir> [vues...] [--samples N] [--res WxH]
Repère : X largeur, -Y avant, Z haut.
"""
import sys, os, math
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
import bpy
from mathutils import Vector
import studio as S
from studio import PI, M
S.reset()
P = S.palette()
mesh_fab = S.mat_mesh_fabric('Maille', (0.014, 0.015, 0.017), scale=32)
mesh_fab.node_tree.nodes['Principled BSDF'].inputs['Specular IOR Level'].default_value = 0.25
cushion_mat = S.mat('TissuRouge', (0.40, 0.012, 0.010), 0.0, 0.72, noise=0.06)
arm_pu = S.mat('PolyurethaneNoir', (0.014, 0.014, 0.016), 0.0, 0.55, noise=0.12)
cushion_mat.node_tree.nodes['Principled BSDF'].inputs['Specular IOR Level'].default_value = 0.22
S.add_weave_bump(cushion_mat, 420, 0.22)
# ---------------------------------------------------------------- piètement
HUB_Z = 0.125
LEG_R = 0.335
legs = []
for i in range(5):
keys = []
for k in range(7):
t = k / 6
x = 0.035 + t * (LEG_R - 0.035)
z = HUB_Z + 0.008 - t * 0.032 + 0.012 * math.sin(t * PI) # légère cambrure
w = 0.030 - 0.011 * t
h = 0.030 - 0.010 * t
keys.append((x, 0, z, w, h, 3.2))
me = S.loft('branche', S.smooth_sections(keys, 4), n=32)
legs.append((me, M(rot=(0, 0, 2 * PI * i / 5 + PI / 2))))
hub = S.lathe('moyeu', [(0, HUB_Z - 0.035), (0.040, HUB_Z - 0.035), (0.048, HUB_Z - 0.01),
(0.048, HUB_Z + 0.025), (0.036, HUB_Z + 0.04), (0, HUB_Z + 0.04)], 64)
legs.append((hub, M()))
S.part('Piètement', S.join_meshes('pietement', legs), P['alu'], 'base',
explode=(0, 0, -0.18), bevel=0.003, label='5-star base')
# embouts sous les branches + roulettes jumelées
W_R, W_T = 0.034, 0.017 # rayon et largeur d'une roue
caster_hub = S.mat('MoyeuGris', (0.10, 0.105, 0.11), 0.0, 0.35, coat=0.4)
for i in range(5):
a = 2 * PI * i / 5 + PI / 2
tip = Vector((math.cos(a) * (LEG_R - 0.012), math.sin(a) * (LEG_R - 0.012), 0))
out = Vector((math.cos(a), math.sin(a), 0))
side = out.cross(Vector((0, 0, 1))) # axe des roues, perpendiculaire à la branche
S.part(f'Embout{i}', S.lathe('embout', [(0, 0), (0.017, 0), (0.019, 0.004), (0.019, 0.028), (0, 0.028)], 32),
P['black'], 'base', loc=tip + Vector((0, 0, HUB_Z - 0.05)), explode=(0, 0, -0.18), bevel=0.002)
ex = out * 0.10 + Vector((0, 0, -0.30))
axle = tip + out * 0.024 + Vector((0, 0, W_R))
wrot = (PI / 2, 0, a) # Z local -> axe des roues ; Y local -> haut ; X local -> extérieur
GAP = 0.020 # largeur du corps central entre les deux roues
# corps central (moyeu) + carénage supérieur qui mord sur le haut des roues
S.part(f'Corps{i}', S.lathe('corps', [(0, -GAP / 2), (W_R - 0.010, -GAP / 2), (W_R - 0.007, -GAP / 2 + 0.003),
(W_R - 0.007, GAP / 2 - 0.003), (W_R - 0.010, GAP / 2), (0, GAP / 2)], 48),
P['black_gloss'], 'roulettes', loc=axle, rot=wrot, explode=ex + Vector((0, 0, 0.04)),
label='Twin-wheel casters · 25 parts')
half = GAP / 2 + W_T * 0.72
fair = [(W_R - 0.006, -half), (W_R + 0.004, -half), (W_R + 0.0065, -half + 0.004), (W_R + 0.0075, -GAP / 2),
(W_R + 0.008, 0.0), (W_R + 0.0075, GAP / 2), (W_R + 0.0065, half - 0.004), (W_R + 0.004, half),
(W_R - 0.006, half)]
o = S.part(f'Carénage{i}', S.lathe('carenage', fair, 48, angle=PI * 0.92, start=PI * 0.04), P['black_gloss'],
'roulettes', loc=axle, rot=wrot, explode=ex + Vector((0, 0, 0.04)))
sol = o.modifiers.new('ep', 'SOLIDIFY'); sol.thickness = 0.0022; sol.offset = 1
# col : pivot vertical posé sur le carénage
S.part(f'Col{i}', S.lathe('col', [(0, 0), (0.0115, 0), (0.0125, 0.004), (0.0095, 0.016), (0.0085, 0.022), (0, 0.022)], 32),
P['black_gloss'], 'roulettes', loc=axle + Vector((0, 0, W_R + 0.004)) - out * 0.010,
explode=ex + Vector((0, 0, 0.04)), bevel=0.001)
S.part(f'Pivot{i}', S.cyl('pivot', 0.0055, 0.030), P['chrome'], 'roulettes',
loc=axle + Vector((0, 0, W_R + 0.034)) - out * 0.010, explode=ex + Vector((0, 0, 0.08)))
for sgn in (-1, 1):
wc = axle + side * (sgn * (GAP / 2 + W_T / 2 + 0.001))
tire = [(W_R - 0.008, -W_T / 2), (W_R - 0.003, -W_T / 2), (W_R - 0.0008, -W_T / 2 + 0.003),
(W_R, -W_T / 2 + 0.006), (W_R, W_T / 2 - 0.006), (W_R - 0.0008, W_T / 2 - 0.003),
(W_R - 0.003, W_T / 2), (W_R - 0.008, W_T / 2), (W_R - 0.008, -W_T / 2)]
S.part(f'Bandage{i}{sgn}', S.lathe('bandage', tire, 64), P['rubber'], 'roulettes', loc=wc, rot=wrot,
explode=ex + side * sgn * 0.05)
hub = [(0.004, -W_T / 2 + 0.001), (W_R - 0.008, -W_T / 2 + 0.001), (W_R - 0.008, W_T / 2 - 0.001),
(0.020, W_T / 2 - 0.001), (0.017, W_T / 2 - 0.003), (0.004, W_T / 2 - 0.003)]
if sgn < 0: # face creusée toujours vers l'extérieur
hub = [(r, -z) for r, z in hub]
S.part(f'Jante roulette{i}{sgn}', S.lathe('jr', hub, 64), caster_hub, 'roulettes', loc=wc, rot=wrot,
explode=ex + side * sgn * 0.05, bevel=0.0008, segs=2)
S.part(f'Enjoliveur{i}{sgn}', S.lathe('enj', [(0, 0), (0.0145, 0), (0.0155, 0.0012), (0.013, 0.003), (0, 0.0035)], 48),
P['chrome'], 'roulettes', loc=wc + side * (sgn * (W_T / 2 - 0.0032)), rot=(sgn * PI / 2, 0, a),
explode=ex + side * sgn * 0.08)
# ---------------------------------------------------------------- vérin
S.part('Fourreau', S.lathe('fourreau', [(0, 0.06), (0.036, 0.06), (0.036, 0.165), (0.033, 0.17),
(0.033, 0.215), (0.030, 0.22), (0.030, 0.27), (0.027, 0.275), (0, 0.275)], 64),
P['black_gloss'], 'verin', explode=(0, 0, -0.08), bevel=0.002, label='Gas lift · 3 parts')
S.part('Piston', S.lathe('piston', [(0, 0.27), (0.0215, 0.27), (0.0215, 0.385), (0.025, 0.39),
(0.025, 0.405), (0, 0.405)], 64),
P['chrome'], 'verin', explode=(0, 0, 0.0))
S.part('Bague', S.tube_ring('bague', 0.03, 0.0215, 0.012), P['alu'], 'verin',
loc=(0, 0, 0.283), explode=(0, 0, -0.04), bevel=0.002)
# ---------------------------------------------------------------- mécanisme
MZ = 0.43
o = S.part('Mécanisme', S.shaped_box('meca', 0.20, 0.25, 0.045, 3,
lambda q: Vector((q.x * (1 - 0.12 * max(0.0, -q.y / 0.125)), q.y, q.z))),
P['alu_dark'], 'meca', loc=(0, 0.01, MZ), label='Synchro mechanism · 18 parts')
S.soften(o, 0.010, 2)
S.part('Platine', S.box('platine', 0.27, 0.30, 0.008), P['steel'], 'meca',
loc=(0, 0, MZ + 0.027), explode=(0, 0, 0.05), bevel=0.003)
S.part('Bouton tension', S.lathe('bt', [(0, 0), (0.022, 0), (0.024, 0.004), (0.024, 0.026),
(0.020, 0.03), (0, 0.03)], 24),
P['black'], 'meca', loc=(0, -0.125, MZ - 0.012), rot=(PI / 2, 0, 0), explode=(0, -0.12, 0), bevel=0.002)
S.part('Tige tension', S.cyl('tt', 0.006, 0.04), P['chrome'], 'meca',
loc=(0, -0.11, MZ - 0.012), rot=(PI / 2, 0, 0), explode=(0, -0.08, 0))
S.part('Levier', S.curve_tube('levier', [(0.10, -0.02, MZ - 0.005), (0.16, -0.03, MZ - 0.01),
(0.19, -0.07, MZ - 0.02), (0.20, -0.12, MZ - 0.025)], 0.0045),
P['chrome'], 'meca', explode=(0.12, 0, 0))
S.part('Poignée levier', S.loft('pl', S.smooth_sections([
(0.0, 0, 0, 0.006, 0.006, 2), (0.02, 0, 0, 0.012, 0.009, 2.5), (0.045, 0, 0, 0.011, 0.008, 2.5),
(0.05, 0, 0, 0.004, 0.004, 2)], 4), n=24),
P['black'], 'meca', loc=(0.20, -0.115, MZ - 0.025), rot=(0, 0, -PI / 2), explode=(0.12, 0, 0))
# ---------------------------------------------------------------- assise
SZ = MZ + 0.04
SEAT_A, SEAT_B, SEAT_H = 0.250, 0.240, 0.075
o = S.part('Coque assise', S.cushion('coque', 0.246, 0.232, 0.024, 5.0, 0.008, 0.006, 0.0), P['black'], 'assise',
loc=(0, -0.012, SZ + 0.010), explode=(0, 0, 0.14), label='Seat · shell + cushion')
def seat_shape(p):
"""Assise sculptée : bourrelets latéraux, creux fessier, bord avant en cascade, avant plus étroit."""
x, y, z = p
if z > 0:
t = min(1.0, z / (SEAT_H / 2)) # 0 au plan médian, 1 sur le dessus
dz = 0.022 * (abs(x) / SEAT_A) ** 3 # bourrelets
dz -= 0.016 * math.exp(-((x / 0.13) ** 2 + ((y - 0.03) / 0.12) ** 2)) # creux
dz -= 0.024 * max(0.0, (-y - 0.12) / 0.12) ** 2 # cascade avant
dz += 0.005 * max(0.0, (y - 0.12) / 0.12) # arrière relevé
z += dz * t
x *= 1.0 - 0.05 * max(0.0, -y / SEAT_B)
return Vector((x, y, z))
SEAT_C = Vector((0, -0.015, SZ + 0.055))
S.part('Coussin', S.cushion('coussin', SEAT_A, SEAT_B, SEAT_H, 4.2, 0.030, 0.010, 0.004, 128, seat_shape),
cushion_mat, 'assise', loc=SEAT_C, explode=(0, 0, 0.30))
seam_m = S.mat('Couture', (0.16, 0.006, 0.005), 0.0, 0.8)
# passepoil sur l'arrondi supérieur
pp = S.cushion_ring(SEAT_A, SEAT_B, SEAT_H, 0.030, 4.2, 128, 0.62, seat_shape)
S.part('Passepoil', S.curve_tube('pp', [SEAT_C + q for q in pp], 0.0034, closed=True, res=2, bevel_res=3),
cushion_mat, 'assise', explode=(0, 0, 0.30))
# piqûres : deux sillons longitudinaux + une traverse, posés dans la mousse
for xs in (-0.092, 0.092):
pts = [SEAT_C + seat_shape(Vector((xs, -0.20 + 0.36 * k / 20, SEAT_H / 2))) - Vector((0, 0, 0.0015)) for k in range(21)]
S.part(f'Couture{xs:+.2f}', S.curve_tube('cou', pts, 0.0026, res=4), seam_m, 'assise', explode=(0, 0, 0.30))
pts = [SEAT_C + seat_shape(Vector((-0.092 + 0.184 * k / 20, 0.16, SEAT_H / 2))) - Vector((0, 0, 0.0015)) for k in range(21)]
S.part('Couture arrière', S.curve_tube('cou', pts, 0.0026, res=4), seam_m, 'assise', explode=(0, 0, 0.30))
# ---------------------------------------------------------------- dossier
BY, BZ, BH = 0.24, SZ + 0.13, 0.56
REC = math.radians(14)
def back_surf(u, v, inset=0.0):
"""u ∈ [-1,1] largeur, v ∈ [0,1] hauteur ; renvoie un point du dossier galbé."""
half = 0.215 + 0.035 * math.sin(v * PI) - 0.03 * v
x = u * (half - inset)
lumbar = 0.028 * math.exp(-((v - 0.22) / 0.16) ** 2)
vv = v * (1 - 2 * inset / BH) + inset / BH
y = BY + vv * BH * math.sin(REC) - lumbar - 0.06 * (u * (1 - inset)) ** 2
z = BZ + vv * BH * math.cos(REC)
return Vector((x, y, z))
outline = []
for k in range(72):
t = 2 * PI * k / 72
u, v = S.superellipse(t, 1.0, 0.5, 5)
outline.append(back_surf(u, v + 0.5))
# cadre sculpté : profil ovale aplati, plus épais sur les flancs (là où passe l'effort), affiné en haut et en bas
outline = []
for k in range(160):
t = 2 * PI * k / 160
u, v = S.superellipse(t, 1.0, 0.5, 5)
outline.append(back_surf(u, v + 0.5))
frame_taper = lambda i, m: 0.78 + 0.30 * abs(math.cos(2 * PI * i / m))
S.part('Cadre dossier', S.sweep('cadre', outline, 0.020, 0.014, 2.6, up=(0, math.cos(REC), -math.sin(REC)),
closed=True, n=28, taper=frame_taper),
P['black_gloss'], 'dossier', explode=(0, 0.30, 0.18), label='Backrest · frame + mesh')
NU, NV = 40, 40
vs, fs = [], []
for j in range(NV + 1):
for i in range(NU + 1):
u = -1 + 2 * i / NU
v = j / NV
uu, vv = u, v - 0.5
# rétrécit la grille dans le contour superellipse
lim = (1 - abs(2 * vv) ** 5) ** (1 / 5) if abs(2 * vv) < 1 else 0
vs.append(back_surf(uu * lim * 0.985, 0.5 + vv * 0.985))
for j in range(NV):
for i in range(NU):
a = j * (NU + 1) + i
fs.append((a, a + 1, a + NU + 2, a + NU + 1))
mm = S.mesh_from('maille', vs, fs)
uvl = mm.uv_layers.new()
for poly in mm.polygons:
for li in poly.loop_indices:
vi = mm.loops[li].vertex_index
uvl.data[li].uv = ((vi % (NU + 1)) / NU, (vi // (NU + 1)) / NV)
o = S.part('Maille', mm, mesh_fab, 'dossier', explode=(0, 0.30, 0.18))
sol = o.modifiers.new('Ep', 'SOLIDIFY'); sol.thickness = 0.002
# support lombaire : coussinet derrière la maille, glissières sur le cadre
lv = 0.24
lsec = []
for k in range(13):
u = -0.80 + 1.60 * k / 12
q = back_surf(u, lv) + Vector((0, 0.024, 0))
lsec.append((q.x, q.y, q.z, 0.011, 0.042 * (1 - 0.35 * abs(u) ** 4), 3.0))
S.part('Lombaire', S.loft('lombaire', lsec, n=24), P['black'], 'dossier', explode=(0, 0.48, 0.14))
for sgn in (-1, 1):
S.part(f'Curseur lombaire{sgn}', S.box('cl', 0.026, 0.03, 0.06), P['alu_dark'], 'dossier',
loc=back_surf(sgn * 0.99, lv) + Vector((0, 0.012, 0)), rot=(REC, 0, 0),
explode=(0, 0.48, 0.14), bevel=0.007)
# colonne : du mécanisme vers l'arrière du dossier, puis fourche (Y) jusqu'aux flancs du cadre
spine = [(0, 0.10, MZ - 0.005), (0, 0.20, MZ - 0.012), (0, 0.30, MZ + 0.03),
tuple(back_surf(0, 0.02) + Vector((0, 0.075, 0.0))), tuple(back_surf(0, 0.12) + Vector((0, 0.07, 0)))]
S.part('Colonne', S.curve_tube('colonne', spine, 0.021, res=32), P['alu'], 'colonne',
explode=(0, 0.20, -0.02), label='Back spine + yoke')
yoke_root = back_surf(0, 0.12) + Vector((0, 0.07, 0))
for sgn in (-1, 1):
arm = [tuple(yoke_root), tuple(back_surf(sgn * 0.45, 0.22) + Vector((0, 0.065, 0))),
tuple(back_surf(sgn * 0.88, 0.40) + Vector((0, 0.04, 0))),
tuple(back_surf(sgn * 1.0, 0.50) + Vector((0, 0.012, 0)))]
S.part(f'Fourche{sgn}', S.curve_tube('fourche', arm, 0.014, res=32), P['alu'], 'colonne',
explode=(sgn * 0.06, 0.20, 0.0))
S.part(f'Rotule{sgn}', S.lathe('rot', [(0, -0.018), (0.016, -0.018), (0.02, -0.006), (0.02, 0.006),
(0.016, 0.018), (0, 0.018)], 32), P['alu_dark'], 'colonne',
loc=back_surf(sgn * 1.0, 0.50) + Vector((0, 0.03, 0)), rot=(0, PI / 2, 0),
explode=(sgn * 0.06, 0.20, 0.0), bevel=0.002)
S.part('Bride colonne', S.lathe('bride', [(0, -0.03), (0.032, -0.03), (0.036, -0.02), (0.036, 0.02),
(0.032, 0.03), (0, 0.03)], 48), P['alu_dark'], 'colonne',
loc=yoke_root, rot=(PI / 2 + REC, 0, 0), explode=(0, 0.20, 0.0), bevel=0.003)
# ---------------------------------------------------------------- accoudoirs
for sgn in (-1, 1):
ex = (sgn * 0.22, 0, 0.05)
bar = [(sgn * 0.09, 0.04, MZ - 0.005), (sgn * 0.20, 0.04, MZ - 0.008), (sgn * 0.27, 0.05, MZ + 0.01),
(sgn * 0.285, 0.06, MZ + 0.06)]
S.part(f'Bras support{sgn}', S.curve_tube('bras', bar, 0.013, res=24), P['alu'], 'accoudoirs',
explode=ex, label='4D armrests · 10 parts')
o = S.part(f'Fût{sgn}', S.shaped_box('fut', 0.032, 0.056, 0.20, 2), P['black'], 'accoudoirs',
loc=(sgn * 0.285, 0.06, MZ + 0.15), explode=ex)
S.soften(o, 0.009, 2)
o = S.part(f'Fourreau accoudoir{sgn}', S.shaped_box('fa', 0.038, 0.062, 0.09, 2), P['black_gloss'], 'accoudoirs',
loc=(sgn * 0.285, 0.06, MZ + 0.105), explode=ex)
S.soften(o, 0.010, 2)
S.part(f'Bouton hauteur{sgn}', S.box('bh', 0.012, 0.03, 0.04), P['alu_dark'], 'accoudoirs',
loc=(sgn * 0.285, 0.029, MZ + 0.19), explode=(sgn * 0.22, -0.06, 0.05), bevel=0.004)
o = S.part(f'Platine accoudoir{sgn}', S.shaped_box('pa', 0.056, 0.20, 0.012, 2), P['alu_dark'], 'accoudoirs',
loc=(sgn * 0.285, 0.04, MZ + 0.255), explode=(sgn * 0.22, 0, 0.12))
S.soften(o, 0.004, 2)
arm_fn = lambda q: Vector((q.x, q.y, q.z + (0.004 * (1 - (q.x / 0.125) ** 2) if q.z > 0 else 0)))
S.part(f'Manchette{sgn}', S.cushion('pad', 0.125, 0.040, 0.030, 3.6, 0.012, 0.005, 0.002, 96, arm_fn),
arm_pu, 'accoudoirs', loc=(sgn * 0.285, 0.03, MZ + 0.277), rot=(0, 0, PI / 2), explode=(sgn * 0.22, 0, 0.20))
# ---------------------------------------------------------------- appui-tête
top = back_surf(0, 1.0)
S.part('Glissière appui-tête', S.box('gat', 0.05, 0.022, 0.16), P['alu_dark'], 'tete',
loc=back_surf(0, 0.88) + Vector((0, 0.03, 0)), rot=(REC, 0, 0), explode=(0, 0.16, 0.22), bevel=0.006,
label='Headrest · 3 parts')
stem = [tuple(back_surf(0, 0.86) + Vector((0, 0.04, 0))), tuple(top + Vector((0, 0.045, 0.02))),
tuple(top + Vector((0, 0.02, 0.085)))]
stem = [back_surf(0, 0.84) + Vector((0, 0.042, 0)), back_surf(0, 0.97) + Vector((0, 0.05, 0.0)),
top + Vector((0, 0.045, 0.05)), top + Vector((0, 0.025, 0.10))]
stem = [stem[0].lerp(stem[1], t / 6) if t <= 6 else None for t in range(7)][:-1] + [stem[1].lerp(stem[2], t / 6) for t in range(6)] + [stem[2].lerp(stem[3], t / 6) for t in range(7)]
S.part('Lame appui-tête', S.sweep('lame', stem, 0.008, 0.026, 4.0, up=(0, 1, 0), n=24), P['alu'], 'tete',
explode=(0, 0.16, 0.30))
def head_shape(p):
"""Repère local du coussin : x largeur, y hauteur, z épaisseur (+z = face avant une fois tourné)."""
x, y, z = p
z += 0.055 * (x / 0.15) ** 2 # galbe enveloppant
y *= 1.0 - 0.22 * (abs(x) / 0.15) ** 3 # oreilles affinées
return Vector((x, y, z))
HEAD_C = top + Vector((0, 0.005, 0.135))
HEAD_R = (PI / 2 + REC * 0.6, 0, 0)
S.part('Appui-tête', S.cushion('appui', 0.150, 0.060, 0.052, 4.0, 0.020, 0.016, 0.003, 112, head_shape), cushion_mat,
'tete', loc=HEAD_C, rot=HEAD_R, explode=(0, 0.16, 0.40))
ring = S.cushion_ring(0.150, 0.060, 0.052, 0.020, 4.0, 112, 0.62, head_shape)
o = S.part('Passepoil appui-tête', S.curve_tube('ppa', ring, 0.0030, closed=True, res=2, bevel_res=3), cushion_mat,
'tete', loc=HEAD_C, rot=HEAD_R, explode=(0, 0.16, 0.40))
# ---------------------------------------------------------------- détails mécaniques
hexb = lambda nm: S.lathe(nm, [(0, 0), (0.0085, 0), (0.0085, 0.006), (0.0055, 0.0075), (0, 0.0075)], 6)
for ix in (-1, 1):
for iy in (-1, 1):
S.part(f'Vis platine{ix}{iy}', hexb('vis'), P['chrome'], 'meca',
loc=(ix * 0.115, iy * 0.13, MZ + 0.031), explode=(ix * 0.03, iy * 0.03, 0.22), smooth=False)
# glissières d'assise
for sgn in (-1, 1):
S.part(f'Glissière{sgn}', S.box('gl', 0.022, 0.34, 0.012), P['alu'], 'meca',
loc=(sgn * 0.10, -0.01, MZ + 0.037), explode=(sgn * 0.02, 0, 0.10), bevel=0.003)
# axe de bascule traversant + carter de ressort
S.part('Axe bascule', S.cyl('axe', 0.009, 0.25), P['chrome'], 'meca', loc=(0, -0.06, MZ - 0.005),
rot=(0, PI / 2, 0), explode=(0.0, -0.04, -0.06))
S.part('Carter ressort', S.lathe('cr', [(0, -0.07), (0.022, -0.07), (0.026, -0.064), (0.026, 0.064),
(0.022, 0.07), (0, 0.07)], 48), P['black_gloss'], 'meca',
loc=(0, -0.06, MZ - 0.02), rot=(0, PI / 2, 0), explode=(0, -0.06, -0.10), bevel=0.002)
for k in range(9):
S.part(f'Spire{k}', S.torus('sp', 0.0265, 0.0028, 48, 8), P['steel'], 'meca',
loc=(-0.056 + k * 0.014, -0.06, MZ - 0.02), rot=(0, PI / 2, 0), explode=(0, -0.06, -0.10))
# second levier (hauteur), côté gauche
S.part('Levier hauteur', S.curve_tube('lh', [(-0.10, 0.02, MZ - 0.005), (-0.17, 0.0, MZ - 0.012),
(-0.205, -0.05, MZ - 0.02), (-0.21, -0.10, MZ - 0.022)], 0.0045),
P['chrome'], 'meca', explode=(-0.12, 0, 0))
S.part('Poignée hauteur', S.lathe('ph', [(0, 0), (0.008, 0), (0.011, 0.01), (0.011, 0.035), (0.008, 0.045), (0, 0.045)], 24),
P['black'], 'meca', loc=(-0.21, -0.10, MZ - 0.022), rot=(PI / 2, 0, 0), explode=(-0.12, 0, 0))
# nervures sous la coque d'assise
for k in range(5):
S.part(f'Nervure{k}', S.box('nv', 0.44, 0.008, 0.014), P['black'], 'assise',
loc=(0, -0.20 + k * 0.095, SZ - 0.005), explode=(0, 0, 0.14), bevel=0.002)
# collier décoratif du moyeu
S.part('Collier moyeu', S.torus('cm', 0.049, 0.004, 64, 10), P['chrome'], 'base',
loc=(0, 0, HUB_Z + 0.022), explode=(0, 0, -0.18))
# plaque signature sur la bride
S.part('Plaque', S.box('plq', 0.05, 0.004, 0.018), P['chrome'], 'colonne',
loc=back_surf(0, 0.12) + Vector((0, 0.103, 0.0)), rot=(REC, 0, 0), explode=(0, 0.26, 0.0), bevel=0.002)
# ---------------------------------------------------------------- rendus
outdir, views, samples, res, tag = S.args('chaise')
WAVES = [['base'], ['roulettes'], ['verin'], ['meca'], ['assise'], ['colonne', 'dossier'], ['accoudoirs'], ['tete']]
WAVE_NAMES = ['Base', 'Casters', 'Gas lift', 'Mechanism', 'Seat', 'Backrest', 'Armrests', 'Headrest']
PENDING = {'dossier', 'tete', 'accoudoirs', 'assise', 'roulettes'}
S.lights(center=(0, 0, 0.6), scale=1.0)
cam = S.camera((0, -3, 1), (0, 0, 0.5), lens=55)
objs = [p['obj'] for p in S.PARTS]
if views[0] == 'video':
import assemble
step = int(sys.argv[sys.argv.index('--step') + 1]) if '--step' in sys.argv else 1
only = None if step == 1 else set(range(0, 100000, step))
if '--range' in sys.argv:
r0, r1 = map(int, sys.argv[sys.argv.index('--range') + 1].split('-'))
only = set(range(r0, r1))
assemble.render_video(os.path.join(outdir, 'video_' + tag), WAVES, WAVE_NAMES, (-1.0, -1.55, 0.42),
fill=0.82, samples=samples, res=res, only=only, total=770, exploded=1.0, ex_spin=False,
ex_focus=[dict(key='accoudoirs', match=r'-1$', yaw=-0.55, dist=1.2,
title='4D armrests', sub='Height · width · depth · pivot'),
dict(key='meca', dist=1.05,
title='Synchro mechanism', sub='4-position tilt lock · tension knob'),
dict(key='roulettes', yaw=-0.25, pitch=0.9, dist=0.62,
match=r'^(Capot|Corps|Carénage|Col|Pivot|Bandage|Jante roulette|Enjoliveur)2(-1|1)?$',
title='Twin-wheel casters', sub='Ø 68 mm · soft-floor PU tread')],
ex_close=1.05, ex_fstop=2.8, ex_wide=34, ex_snap=10, orbit=1, end_hold=0,
ex_labels={'base', 'assise', 'dossier', 'tete'},
# vue éclatée « logique » : pile verticale, dossier en arrière, accoudoirs sur les côtés
ex_groups={'base': (0, 0, -0.42), 'roulettes': (0, 0, -0.42), 'verin': (0, 0, -0.16),
'meca': (0, 0, 0.0), 'assise': (0, 0, 0.24), 'colonne': (0, 0.20, 0.04),
'dossier': (0, 0.38, 0.20), 'accoudoirs': (('side', 0.34), 0, 0.10),
'tete': (0, 0.42, 0.55)})
sys.exit(0)
a2 = 2 * PI * 2 / 5 + PI / 2
ZOOMS = { # vues rapprochées fixes pour les planches avant | après
'z_assise': (Vector((0.0, -0.08, SZ + 0.04)), Vector((-0.50, -0.80, 0.34)), 50),
'z_roulette': (Vector((math.cos(a2) * LEG_R, math.sin(a2) * LEG_R, 0.05)), Vector((-0.20, -0.36, 0.12)), 60),
'z_accoudoir': (Vector((-0.20, 0.02, MZ + 0.10)), Vector((-0.62, -0.78, 0.26)), 50),
'z_dos': (Vector((0.0, 0.30, MZ + 0.30)), Vector((0.55, 0.95, 0.35)), 45),
}
for v in views:
S.explode(0.0)
cam.data.lens = 55
if v in ZOOMS:
tgt, off, lens = ZOOMS[v]
cam.data.lens = lens
cam.data.shift_x = cam.data.shift_y = 0
cam.location = tgt + off
S.look_at(cam, tgt)
if v == 'hero':
S.frame_camera(cam, objs, (-1.0, -1.55, 0.36), 0.86)
elif v == 'side':
S.frame_camera(cam, objs, (1.0, 0.05, 0.12), 0.84)
elif v == 'back':
S.frame_camera(cam, objs, (0.9, 1.3, 0.5), 0.84)
elif v == 'explode':
S.explode(1.0)
S.frame_camera(cam, objs, (-1.0, -1.55, 0.55), 0.84)
elif v == 'sketch':
S.explode(0.7)
S.frame_camera(cam, objs, (-1.0, -1.55, 0.55), 0.74)
S.sketch_state(PENDING, 0.7)
elif v == 'detail':
cam.data.lens = 85
cam.data.shift_x = cam.data.shift_y = 0
tgt = back_surf(0.55, 0.12)
cam.location = tgt + Vector((-0.55, -0.75, 0.22))
S.look_at(cam, tgt)
elif v == 'low':
S.frame_camera(cam, objs, (-0.6, -1.6, -0.05), 0.84)
S.render(os.path.join(outdir, f'{tag}_{v}.png'), samples, res)
if v == 'sketch':
S.export_labels(os.path.join(outdir, f'{tag}_{v}.json'))
S.restore_materials()
S.explode(0.0)
S.save_blend(os.path.join(outdir, 'chaise.blend'))
Example: an office chair, built part by part by its Blender Python program.
Example: a pack of macarons, built by its program, then finished as packshots.
Example: a gantry stair, from its laser scan to separate 3D parts.
Example: a house, a motorbike, a chameleon and a castle, each blocked out in 3D, then generated.
Example: a street bench built in code, finished in Blender, placed along a rally road, then varied.
Every object comes out
parametric and editable, ready to drop into production.
// Coming soon
Next-gen is loading...Our first model drops soon.
A new 3D modeling technology built on code.
Every object takes shape part by part.