jeanrojas.com

Footer

jeanrojas.com

Boosting remote teamwork and improving systems architecture focusing on team communication patterns.



jrojastechnology@gmail.com
+1 (929) 2245443

Links

  • About
  • Experience
  • Blog
  • Contact

Social

  • Github
  • Codepen
  • Linkedin
  • Twitter
  • Behance
  • Quora
  • AdpList

Subscribe to my newsletter

The latest news, articles, and resources, sent to your inbox weekly.

© Jeanrojas.com All rights reserved.

← All articles
Final render from the stage of the rebuilt Stern Auditorium, looking out over the parquet, the tiers and the domed ceiling.
Final render: the view from the stage. Cycles, 1920×1080, 14 mm camera, with the stage back wall hidden for this shot only.

September 16, 2026 · 33 min read

Carnegie Hall, rebuilt from a seating chart

How I turned a flat ticketing map into a complete Stern Auditorium: 2,758 seats, four tiers of gilded parapets, vaulted ceilings and hundreds of real light fixtures. I did it by building tools instead of sculpting.

On this page

Technical art breakdown · 27 July – 16 September 2026 · Blender 5.2 · Houdini 20.5 · Cycles

2,758
seats placed and aligned to the terraces, from a chart listing 2,804
9
procedural Houdini asset builders, 6,745 lines of Python
17k
lines in the ornament library that dresses every parapet
5.6M
unique triangles, 13.6M rendered, across 3,170 objects
~700
lights in the final scene, placed at modelled lamps and fixtures

I'm moving my career toward technical art. The part of this work I enjoy most is not the final pixel but the system that produces it. So I set myself a deliberately awkward brief: recreate the Isaac Stern Auditorium at Carnegie Hall as a real-time-ready environment, and start from nothing but a seating chart and reference photos. No scans, no architecture asset packs, no sculpting.

That constraint shaped everything. A seating chart is a precise 2D record of the hall, since every seat, box and aisle is in the right place, and it contains no height, no ornament and no light at all. The job was to lift that data into three dimensions. Then I dressed it with a vocabulary of classical ornament generated from code, and lit it only with lamps that exist in the model. This post walks through that pipeline in the order I built it, including the parts that failed.

Prelude

How I worked

Almost nothing in this hall was pushed into shape by hand. Every asset comes from a script: Houdini builders run headless through hython, and Python drives Blender live through the Blender MCP bridge. I used Claude Code as a scripting copilot that wrote and ran that code against my scenes. My job was the part a technical artist owns: breaking the building into systems, choosing the technique for each, reading the reference, setting the rules, and judging every result against the photos until it was right.

The feedback loop mattered more than any single tool. I drew directly on viewport screenshots, with blue lines for paths and red circles for problems, and those drawings became the input to the next build.

  1. ReferencePhoto of the real detail, cropped to one element.
  2. AnnotateBlue path or red circle drawn on a viewport screenshot.
  3. BuildA parametric script generates the geometry.
  4. MeasureRatios, areas, volumes and open edges checked as numbers.
  5. ReviewRender, compare with the photo, give one precise note.
Blender top view of a U-shaped tier with a hand-drawn blue line
InputA hand-drawn blue line marks where the second-tier terrace step should run.
Close-up of a tier end with hand-drawn blue arcs
InputArcs sketched over the end of the U. The build fitted them to within 2.5 cm.
Parapet pilaster with a red circle drawn around it
Note“These small columns are too flat.” A red circle is often the whole brief.

Stage 1 · 27–30 July

From seating chart to vectors

The official Stern Auditorium seating chart is a vector PDF, which makes it a survey drawing that happens to sell tickets. I converted its two pages to SVG. Page one holds the parquet and the first and second tiers. Page two holds the dress circle and the balcony. From there I pulled out two kinds of data.

Carnegie Hall's official seating chart, page one: the parquet, the Blavatnik Family First Tier boxes and the Second Tier boxes around the stage
Source · page 1Parquet, Blavatnik Family First Tier and Second Tier. Every seat, box and aisle is drawn in place.
Carnegie Hall's official seating chart, page two: the dress circle and the centre and side balcony
Source · page 2Dress circle and balcony, the two upper levels.
Colour ticketing map of the hall with the stage on the left, showing parquet, tier box, dress circle and balcony sections
Source · section mapThe same hall as a colour ticketing map, stage on the left, with every level in one view.

Architecture: strip everything that isn’t a wall

The full page-one SVG carries 4,913 paths and 5,948 text elements, because every seat is a numbered box. I produced a second file that keeps only the architectural strokes: the stage house, the parquet walls, the curve of each tier and the radial lines that divide the boxes. That outline file, 1,514 paths and no text, is the floor plan everything else was traced against.

Line drawing of the auditorium plan with stage house, parquet and tier box divisions
Outline SVGThe chart reduced to architecture: stage house, parquet walls, and the radial box partitions of both tiers.
Dots on a dark background forming the seat layout of the hall, coloured by section
Seat data2,864 seat symbols reduced to centre points in PDF units, clustered by section. Left is page one with the three parquet blocks and the boxes. Right is page two with the dress circle and balcony. Grey dots are unclustered noise.

Seats: every symbol becomes a point

Each seat on the chart is a small closed shape. I took the centroid of every one and wrote it to a CSV with its page, a cluster id and x/y in points. Clustering by proximity grouped the seats into their real sections without reading a single label. On page one, the three parquet blocks came out as clusters of 310, 311 and 385 seats, and each tier box came out as its own group of about eight. That file is the seat layout. Scaled to metres, those points drove seat placement in the first Blender scene, which held 2,801 seat instances.

Blockout layers

For the tiers I redrew the plan as clean, named layers in a vector editor: OUTLINE, BASE, FLOOR and BOXES, with each of the 33 box partitions as its own shape. A second map colour-codes the dress circle rows by terrace step. Those colours are the data that later told each row which step height it sits on. Imported into Blender and extruded, these layers became the first low-poly blockout: floor slabs, terraces and partitions made from primitives.

Coloured vector drawing of a U-shaped tier with named layers
Blockout SVGTier plan as layers: base (green), floor (red), boxes (lilac).
Dress circle plan with rows coloured in bands
Row mapDress circle rows coloured by terrace level.
Parquet plan with three seat blocks and the stage
ParquetThe three parquet blocks against the stage.

By 3 August the blockout could be lit and rendered. It’s crude, with a placeholder stage wall and flat red seats, but every seat and tier is already in its surveyed place. From here on, the work was about replacing primitives with the real thing, one system at a time.

Early render of the horseshoe tiers with red seats
3 AugFirst light on the blockout.
Early render looking at the tiers from the parquet
3 AugTiers from the parquet.
Early render of a placeholder stage wall
3 AugA placeholder stage, later replaced entirely.

Stage 2 · 30 July – 3 August, revised 1 September

A procedural hero-asset kit in Houdini

The hall repeats a small set of architectural pieces many times: tier columns, consoles, arches, doors and ceiling panels. Each one got its own Houdini builder: a Python script that constructs the whole SOP network from scratch, renders previews in Karma and exports OBJ, FBX and GLB. They share one helper library, so every asset uses the same materials, conventions and export fixes. Nothing is baked, and every asset rebuilds with one command.

"C:\Program Files\Side Effects Software\Houdini 20.5.370\bin\hython.exe" build_arch.py --export --final

The same few ideas carry the whole kit:

  • A silhouette is one profile curve. A wrangle turns the profile’s X into pscale, a chamfered cross-section is copied to those points, and skin lofts the body. A primitive wrangle maps each face ring back to its profile segment, which assigns gold or ivory, so retiming a moulding keeps its gilding.
  • An ornament is a template plus copy-to-points. Beads, reeds, dentils and egg-and-dart are single small meshes laid along a resampled curve, so a longer arc simply gets more beads at the true size.
  • Proportions are measured, not eyeballed. Each builder reads its ratios off the reference photo and prints a reference-versus-model table on every run.

The tier column

The first asset was the gilded pilaster that stands between the boxes. The final version has 17,108 polygons in two PBR materials. It also taught the first hard lesson. My early acanthus leaves were assembled from flat plates, and at render distance they read as fir trees or scattered debris. Light breaks at every seam between separate pieces, so no arrangement of them looks carved.

Reference photo of an ivory and gold pilaster
Reference
Render of the procedural pilaster with gold capital
HoudiniKarma preview.
Close-up of flat gold leaf shapes reading as shards
FailureFlat plates read as shards.
Close-up of a gold leaf shape that looks like a fir tree
Failure…or as a fir tree.

The GLB export needed its own fix, and I folded it into the shared library so every later asset got it for free. Houdini’s glTF ROP leaves doubleSided unset, so single-sided leaf ornaments vanished under backface culling. It also drops emission, so lamps came out as dead grey plastic. A small post-process patches the binary container directly:

build_column.py
def patch_glb(path, mesh_name=None, emissive=None):
    ...
    js = json.loads(chunks[0][1].decode("utf-8"))
    for m in js.get("materials", []):
        m["doubleSided"] = True
        pbr = m.setdefault("pbrMetallicRoughness", {})
        pbr.setdefault("metallicFactor", 1.0)
        # the exporter drops emission, so lamps come back as dead grey plastic
        if emissive and m.get("name") in emissive:
            m["emissiveFactor"] = emissive[m["name"]]
            m["extensions"] = dict(m.get("extensions", {}),
                KHR_materials_emissive_strength={...})

The arch: one knob instead of many

The ornamental arch is one moulding profile, in radius and depth, swept around the arch path. That single loft produces the bead astragal, the rinceau band, the coffered splay and the soffit reveal. The art direction came as short notes: make the opening bigger, stop the frame from extending past the arc, make it 10% wider. Each note became one parameter, and the rest of the build derives from it.

build_arch.py
R_SHIFT = 1.350      # pushes the band outward; frame, jambs, ornament counts, lights and cameras follow
FRAME_W = 0.420      # moulded border width along the top
HX      = R_OUT      # half-width IS the arch radius: no dead panel beside the jambs
WIDEN   = 1.10       # final X-only stretch, pivoted on the arch axis
Arch version one with heavy gold band
v1Band too heavy.
Arch version two with a larger opening
v2Inner radius +34%.
Arch version three inscribed in the frame
v3Arch inscribed in the frame.
Arch version four, ten percent wider
v4+10% width, one parameter.
Close-up of the arch mouldings: bead runs, rinceau band and coffers
DetailEvery run on the band is generated along the same curve, so nothing terminates in mid-air.

The ceiling rose

The ceiling panel is a radial problem, so it’s one radial profile swept around a 72-segment circle, with the flat surround lofted from the circle out to the rectangle. I measured the reference plate against the panel’s half-width and drove every ring from those fractions:

Feature (fraction of half-width)ReferenceModel
Outer bulb ring0.9400.940
Beaded ring, outer0.9250.925
Foliate annulus, outer0.8930.893
Foliate annulus, inner0.6750.675
Dense light ring0.5990.599
Dome springing0.5630.563

Matching ratios wasn’t enough. The first version was square with thin ornament, while the real panel is a rectangle with side panels and lamps in the inner cove. The rebuild measured the bay proportions: the centre coffer is 56.1% of the width and each side panel 19.8%. It also replaced a spiky leaf fan with a continuous rinceau vine and added 96 upright lamps in the cove, exported with real emissive strength.

Reference photo of the ornate ceiling panel with a ring of lights
Reference
First version of the ceiling panel, square
v1Right ratios, wrong object.
Final ceiling panel, rectangular with side panels and lit rings
FinalRectangular bay, rinceau vine, emissive lamps.

Doors: exit and box

The exit door architrave uses a neat trick: an offset of a rectangle is just a bigger rectangle, so lofting one offset outline per profile step gives exact mitred corners with no sweep and no boolean. The EXIT sign is a font SOP extruded into a housing with an emissive red material. When I later asked for the sign to sit inside a taller frame, the height was split into DOOR_OPEN_H plus TRANSOM_H. The architrave and its ornament simply ran taller, and the door leaves stayed bit-identical.

The box doors reuse the same library. The frame band measured 0.201 of the opening width, which is the same ratio as the exit door. Its bead-and-reel is one repeat copied along the frame path, and the three raised panels are placed from fractions of the leaf height read off the photo.

Reference photo of a double exit door with an EXIT sign
Reference
First exit door build with the sign mounted above the frame
v1Sign above the frame.
Revised exit door with the sign inside a taller frame
v2Sign moved into a transom.
Close-up of the transom with rinceau and a glowing EXIT sign
DetailRinceau reused at 3× scale.
Reference photo of a panelled box door
Reference
Render of the procedural box door
Houdini24,548 polygons, 3 materials.
Close-up of a brass doorknob and bead-and-reel frame
DetailBrass hardware.
Close-up of the graduated bead mouldings on the frame
DetailThree graduated bead runs.

The fluted column, and choosing ornament that code does well

The balcony columns taught the most useful lesson of the project. The shaft flutes are not booleans. The shaft is lofted plain, and one wrangle pushes each point inward by a cosine of its angle, with an amplitude that fades in and out with height. The ring has 96 points, exactly four per flute, so groove bottoms and fillets both land on samples.

build_fluted_column.py — point wrangle (VEX), simplified
float th = atan2(@P.z, @P.x);
float r  = length(set(@P.x, 0, @P.z));
r -= flute_amp   * 0.5 * (1.0 + cos(24 * th));  // shaft: 24 flutes cut in
r += gadroon_amp * 0.5 * (1.0 + cos(16 * th));  // capital: 16 gadroons raised out
@P.x = r * cos(th);
@P.z = r * sin(th);

The capital was the problem. Three attempts at hand-built acanthus went from flat plates, to exploding shards, to a lofted leaf surface that still read as crushed rock. So I changed the brief rather than the technique. I kept the classical vocabulary and chose an ornament that is a surface-of-revolution problem: gadrooning, a ring of convex lobes. It’s the same wrangle with the sign flipped. It reads as carved because it is one continuous surface, and it came out lighter at 11.2k polygons.

Capital attempt one with flat leaf plates
Try 1Flat leaf plates.
Capital attempt two with shards of leaves
Try 2Leaflets exploded.
Capital attempt three with lofted leaves
Try 3Lofted, still rocky.
Final capital with smooth gadroons and gold volutes
FinalGadroons: one surface.
Lesson

When a procedural approach keeps failing, question the brief before the technique. The right ornament is the one your toolset can make read as carved.

The console bracket

The pilaster with a scrolled console and a dentil cornice was the fussiest proportion problem. A moulding that turns a corner offsets its plan rather than scaling it, so the cornice plan is a function outline(u) that every profile sample rides, and the mitres fall out for free. The iterations were almost all about which way the cornice block runs: its width comes from the column, and its long axis goes into the depth.

Reference photo of a pilaster with a gilded console bracket
Reference
Render of the procedural console pilaster
HoudiniAbout 4.8k polygons in the final version.
Close-up of the scrolled console with gold rope and dentil cornice
DetailSwan-neck scroll, 3-strand rope, returned dentils.

The proscenium: fitting a shape, then re-bending ornament onto it

The great arch over the stage is where measurement paid off most. My first build assumed a circular segment. For a segmental arc the radius follows from span and rise:

R = (S²/4 + f²) / 2f        # span S, rise f: rise/span 0.1255 gives R = 12.75 m at a 12 m span

It still looked wrong at the ends. So I modelled the correct outline as a simple band in Blender and exported it as the ground truth. Walking its boundary loop and fitting curves showed why:

My first buildBlockout band (truth)
CurveCircular arcSemi-ellipse, a = 14.8, b = 3.2
Rise / span0.1540.108
Tangent at springing34°Vertical
Fit error–0.0024 over 200 points

A circle can’t land vertically, and that was the error. Instead of remodelling, I took the finished ornamental arch and swapped its semicircle for the ellipse in the four functions it hangs everything on. A “radius r” became an offset along the ellipse normal, and ornaments were spaced by true arc length. One uniform scale of 2.2335 made the two band widths agree, so the longer curve simply carries more ornament at the same size: 51 coffers, 63 rinceau motifs and 139 palmettes. The last pass made the section symmetric so both faces are moulded. That pass also exposed a classic gotcha: Houdini’s Mirror SOP outputs the original and the reflection. Merging it beside the original had tripled the ornament, 135,966 polygons instead of 94,804, and the arithmetic gave it away.

First proscenium: a circular arch on two columns
v1Circular arc on full columns.
Second proscenium: fluted shaft, console and dentil impost
v2Shaft, console, impost cornice.
Final proscenium springing with coffered soffit and gold rinceau
FinalArch ornament re-bent onto the fitted ellipse.
Close-up of the proscenium band: palmettes, bead runs and coffers
DetailCoffers and palmettes spaced by true arc length along the ellipse.

Stage 3 · 3 August & 1 September

Making inherited geometry trustworthy

A blockout extruded from a chart is not clean. The terraces were soups of disconnected quads, ornament strips were open shells, and several objects were fused together. This is the unglamorous core of technical art: you can’t build on geometry you can’t trust, and you can’t trust it until a number says so. Every fix below ended with a measurement, not a glance.

  • Symmetry by cut and mirror. The dress circle was asymmetric. An Exact-solver boolean removed one half, and the cutter’s edge sat 1.7 mm past centre, so I snapped the seam vertices to exactly x = 0 before mirroring. Left alone, that becomes a 3.4 mm band of doubled faces down the middle.
  • When the boolean lies. The balcony cut reported success but left 39 tiles inside the cutter. The cause was 964 duplicate coincident faces, which make the winding number ambiguous. The fix removed the duplicates, deleted every face inside the cutter with a ray-cast parity test, and bisected the straddlers.
  • Retopology by constrained Delaunay. A broken patch with faces of up to 22 sides was rebuilt from its boundary loop, with 130 edge vertices pinned as constraints and a fresh triangular lattice inside. The minimum angle went from 1.13° to 2.67°, the median to 41°, and the area was unchanged to four decimals.
  • Flat footprints of stepped floors. Flattening a terrace stacks each tread on the soffit above it. Solidifying, running an Exact self-union, bisecting and filling gave a clean plate. I checked it with 3,000 random point samples: none fell outside the original silhouette.
  • Splitting fused objects without crashing Blender. The tier copings were one object made of 5,827 six-vertex fragments, and an edit-mode separate took Blender down. The rebuild split by Z bands with from_pydata and no operators, which was lossless: 23,332 vertices in, 23,332 out.
  • Closing open shells. Swept ornament strips had no back faces, with 764 boundary edges. Because the back was a vertical plane, every top-back vertex paired exactly with a bottom-back one, so each strip closed on its original vertices. That left zero boundary edges and a positive signed volume.
Broken terrace geometry highlighted in orange
BeforeA patch that didn’t follow the tiers.
Top view of box partitions arranged in a U
Partitions30 partitions, each framed by its PCA long axis.
Consoles placed on each partition
PlacementConsoles aligned to each outer face within 0.02°.
Velvet armrests extruded on top of partitions
ArmrestsPadded rails swept from the coping profile.

Reusing profiles precisely is also a cleanup skill. To put the tier-one ornaments onto tier two, I didn’t eyeball a new sweep. I read the cross-sections from the tier-one solids’ end caps. Their shoelace areas matched the measured caps: 0.17031 against 0.17030. The sweep was accepted only when its volume matched section area times path length: 10.4441 m³ built against 10.4447 in theory. That volume check caught two wrong attempts where the sweep folded or ended up on the wrong side of the path.

Stage 4 · 26 August

Seating 2,758 people

The seat layout came from the chart, but the seats themselves needed a system. Real auditorium seats share armrests, so a row of N seats has N+1 armrests. I modelled one seat with only its right armrest, making it tileable, plus a separate end armrest. The geometry confirmed the idea: the standalone armrest is an exact copy of the seat’s own, offset in X by 10.192385 units, and that offset is the tile period.

So I baked both meshes into a normalized frame where the tile width is exactly 1.0. The seat’s armrest straddles +0.5, and the end armrest straddles −0.5. Object scale then equals row pitch in metres, and the end armrest drops in at the last seat’s exact transform. The script detected 79 parquet rows, swapped mesh data without moving any object, and capped every row.

A row of tileable red seats sharing armrests
TilingOne-armrest seat, one end cap per row.
A tall bar chair used in the boxes
BoxesBox seats swapped for bar chairs, scaled to the old footprint.
An arc of chairs following a tier
Tier 1259 chairs around the U.

The upper levels were harder, because the chart has no heights. In the dress circle, grouping seats by geometry instead of by name showed all 443 already sat on seven clean arcs. The block was simply floating. The top step is 1.6 m deep and carries two rows, while the layout had assumed one step per row. The terrace’s top faces also had inverted normals, so they didn’t register as floor. A radial ray-cast scan recovered the true staircase instead.

The balcony looked fine from the side and chaotic from above. Checking heights and symmetry wasn’t enough; the real reference was the terrace itself. The final fix extracted the step edges, where a horizontal tread meets a vertical riser, and filtered out radial edges so seats wouldn’t snap onto aisle steps. Each misaligned seat then took its position from the nearest step arc and its heading from that arc’s tangent.

Balcony alignment to step arcsBeforeAfter
Median angle error0.88°0.86°
Worst seat88.6°7.44°
Seats over 8°260
Front row distance to stepup to 3.47 m0.328 m
Top view of balcony seats coloured by block
DiagnosisSeats coloured by detected block. A bimodal gap histogram revealed 42 narrow aisles.
Top view of misaligned balcony rows
BeforeRows cutting across the step arcs.
Top view of balcony seats aligned to terrace arcs
AfterEvery seat on its tread, mirror error 0.0.
AreaSeatsEnd armrests
Parquet99479
Dress circle44341
Balcony814142
First tier boxes259–
Second tier boxes248–
Total2,758262
Render of the hall with all seats placed, before ornament
26 AugEvery seat placed. The architecture is still primitives, and that changes next.

Stage 5 · 14 September

Closing the room

With seats in place, the hall still had no back walls, no underside to the balconies and no ceiling beyond the ornate panel. I built all of it from the tier outlines themselves, one drawn line at a time.

  • Box terraces. The real boxes are raked, so tier two got one 0.18 m step and tier one got two, placed halfway between seat rows. Partitions and armrests were cut along the same path and raised with their seats. The ends of the U got arcs fitted to my sketch.
  • Walls from floor edges. The outer walls follow each floor’s own boundary points, so the curve has no gaps. Every wall tucks 5 mm past the floor edge to avoid z-fighting.
  • Consoles and doors by face normal. I selected the partition end faces, and a copy of the tier-one console was aligned to each normal. Then 23 box doors were centred in the gaps between them.
  • A funnel soffit. Under the balcony, the slope at each point around the U is the steepest that stays 0.30 m under every step, smoothed along the curve, just like the photo.
  • Vaults from the proscenium curve. The vault ceilings reuse the exact 241-point curve of the arch soffit, extruded back over the balcony and forward over the stage, then trimmed to the walls.
Box terraces with raised back rows
TerracesRaised back rows.
White wall wrapping the second tier
WallBuilt along the drawn line.
Box interiors with consoles and doors
BoxesConsoles and doors placed by normal.
Funnel-shaped soffit mesh under the balcony
SoffitThe funnel under the balcony.

Bending the ornate ceiling panel along the arch exposed a subtle bug. The deform moved only vertices, and the panel’s flat background was made of a few enormous faces, some spanning the full 22.5 m. Those stayed flat and cut across the curve, swallowing the gold mouldings that sit a few millimetres proud. The fix sliced every face wider than 20 cm into strips before bending. It touched only the plain background and left the ornament alone.

Ceiling bent along the arch with mouldings disappearing
BugMouldings swallowed by long flat faces.
Ceiling bent correctly with the dome and mouldings intact
FixSlice the background, then bend.
Top view of the complete hall plan
PlanThe closed hall from above.

Stage 6 · 14–15 September

An ornament system, not ornaments

The tier parapets are the face of the hall. There are four of them, each with a different treatment, running for hundreds of metres of curve. Hand-modelling them was never an option, and building one parapet as a one-off would have meant doing it four times. So I built a library.

Spec first

I ran the design as a small multi-agent workflow. Two independent proposals, one photo-faithful and one classically buildable, were scored on likeness, architecture, consistency and triangle budget. The photo-faithful design won, 31 to 29.5 out of 40, and its best ideas were merged into a single spec.json. The spec is the contract. It defines the body as one closed profile polygon, a triangle budget per ornament unit, and the rules that keep everything aligned:

spec.json (excerpt)
"band_fractions_photo": { "rail": 0.05, "top_cap": 0.08, "frieze": 0.42,
                          "cornice": 0.23, "brackets": 0.16, "bottom": 0.06 },
"tri_budgets": { "egg_dart_unit": 88, "dentil_unit": 24, "bead_reel_unit": 40,
                 "bracket": 560, "target_total_max": 900000 },
"alignment":   "pilaster axes are the module: on every pilaster axis centre one bracket,
                one dentil, one cornice egg ... between two axes use n = max(1, round(L/pitch))",
"min_feature": "visible parts >= 8 mm (about 6 mm/px at 10 m) ..."

Generators, each with its own reviewer

Six generator modules each own one family of ornament: profiles and sweeps, egg-and-dart, dentils and bead-and-reel, scroll brackets, rinceau scrollwork, and the panel and bracket layout. Each was checked by an independent reviewer script before assembly. The assembler lays the pieces along the tier’s real path, recomputing the pitch on the parallel curve at each detail’s own depth, and passes its own suite of 61 tests. The library totals about 17,000 lines of Python, reviewers included.

Reference photo of the first tier parapet with gilded panels and brackets
ReferenceFirst tier parapet.
First generated parapet with gold panels, pilasters and brackets
First build39 wide panels, 40 pilasters, 251 brackets, about 762k triangles, mirror-symmetric and closed.

Because every dimension lives in the spec, art direction became cheap. “The small columns are too flat” changed one number, from 1.8 cm to 5.5 and then 15.5 cm. The cap blocks, rail overhang and brackets all followed. Adding velvet cushions on each pilaster was a new closed solid that reused the rail’s section.

Flat pilaster on the parapet
BeforePilaster 1.8 cm proud.
Parapet with deep pilasters and velvet cushions
After15.5 cm proud, with velvet cushions.
Hall view with tier parapets and seats
ContextThe parapets in the hall.

Four variations from one vocabulary

Once the parts existed, each tier became a recipe:

  • Parquet wall cornice. The parapet’s lower section runs 84 m around the walls, with 102 lamps in a pattern of one lamp, then two empty bracket gaps.
  • Second tier. No separators. Each bay is one wide panel bowed 8–10 cm toward the hall, with a bracket pair at each junction and a lamp centred under every bay.
  • Dress circle. No brackets or scrollwork. Plaster separators and rows of long oval slots cut all the way through the body, so you can see the seats behind them.
  • Balcony. The dress circle recipe without lamps, rebuilt on the floor’s true front edge.
Reference photo of the second tier with bowed panels and lamps
ReferenceSecond tier.
Generated second tier parapet with bowed panels and lamps
BuildBowed bays, bracket pairs, one lamp per bay.
Reference photo of wall cornice lamps between brackets
ReferenceLamps between brackets.
Generated cornice with lamps between brackets
BuildParquet wall cornice with 102 lamps.
Dress circle parapet with oval slots and lamps
Dress circleSeparators, oval slots, lamps in the band.
Close view of see-through slots
DetailSlots cut through, ivory throughout.
Balcony and dress circle parapets seen from below
BalconySame recipe, no lamps.

Composing, not deforming: the arch corbels

The proscenium arches ended in open geometry where they met the walls. My first fix bisected and stretched an existing stage column, and it looked awful. Deforming a finished mesh destroys the proportions its detail depends on. The redo composed the corbel from the library’s parts, stacked to follow the photos: a dentil cornice, a rinceau frieze, a small egg-and-dart, three large S-scroll consoles (the parapet brackets scaled up), a bead-and-reel plinth, and a thin pilaster fading back into the wall. That became a standing rule for the project. New pieces are composed from the generators and never stretched from existing meshes.

Reference photo of a corbel with three scroll consoles
Reference
Arch end resting on a plain block
BeforeThe arch end on a plain block.
Composed corbel with three scroll consoles and pilaster
ComposedBuilt from parapet parts.
Corbel and coffered arch from below
In placeUnder the coffered soffit.

The same parts dressed the rest of the ceiling. The side band went through four versions before it matched the photo: a scroll frieze, a divider, dentils, egg-and-dart, and a new reusable palmette row. The arch soffits got alternating coffers with rinceau and oval medallions, laid out along each arch’s own curve.

Reference photo of the ceiling border band
ReferenceCeiling border.
Generated ceiling band with scroll frieze and palmettes
BuildVersion 4 of the band.
Arch soffit coffers seen from below
CoffersSoffit coffers along the arch.

Stage 7 · 3 August – 16 September

Light only where a lamp exists

My first lighting pass used 118 lights and looked like an office: 138 kW with fill lights everywhere, so nothing fell off and nothing was dark. The fix was subtractive. I cut total power 3.4 times, dropped the fill from 350 W to 55 W so the under-balcony reads dark, warmed the house to about 2400 K, and switched to AgX Medium High Contrast.

Later I adopted a stricter rule: every light must come from a fixture you can see. The dome LEDs were read straight from the emissive faces of the ceiling mesh, with 96 in the inner ring and 40 in the outer, each light 6 cm below its lamp. Stage washes and front-of-house lights hanging in mid-air were deleted. Every new fixture shipped with its light, sharing one datablock per family so a whole ring can be tuned at once.

Fixture familyCountSource
Dome LEDs136Read from the ceiling’s emissive faces
Stage ceiling LEDs112Oval tray rings and front row
Parquet cornice lamps102One per three bracket gaps
Arch downlights102Two rows along the proscenium arches
Dress circle lamps59Set in the parapet band
Second tier lamps37One per bowed bay
Back beam LEDs28Behind the balcony ceiling beam
Balcony soffit domes26Outer ring, with 13 air vents inside it
Box door lamps23Above each tier-two door

The seats got a velvet material with a sheen lobe, mottled colour and a fine pile bump. It was rebuilt in place on the one shared material, so about 2,700 seat objects updated at once. The hero panorama rendered in Cycles at 3840×2033 with up to 4,096 samples and 32 bounces on an RTX 3090, and took 61 minutes. One practical lesson from that render: a render launched from a script freezes Blender’s UI and shows no progress, so long renders go through F12.

Stage ceiling with oval tray, LEDs and cornice
StageOval tray, LED rings, cornice.
Balcony ceiling beam with a row of downlights
BeamPanelled beam with LEDs.
Close view of red velvet seats in the parquet
VelvetSheen, mottling and pile bump.

Coda

Is it game-ready?

The scene renders 13.6 million triangles, but only 5.6 million are unique: 3,170 objects built from 167 distinct meshes. The seats alone account for 5.3 million rendered triangles, which becomes a few thousand once they are exported as instances. For Unreal Engine 5 that is a comfortable Nanite workload, as long as repeated pieces stay instanced and the procedural velvet is baked or rebuilt as an Unreal cloth-sheen material. Every Houdini asset already exports clean GLB with real-world scale, Y-up and patched materials. That is where the project goes next: into Unreal, with a MetaHuman opera singer on this stage.

What I’d tell another technical artist

Start from data, not a mood board. A seating chart gave me survey-grade positions for 2,864 seats and every wall for free. Look for the precise document hiding behind your subject.

Measure, then print the proof. Every builder printed reference-versus-model ratios. Arguments about whether something looks right end quickly when both numbers are on screen.

One note, one parameter. “10% wider” should be one edit. If a note forces you to touch many values, the parameterization is wrong.

Verify geometry with numbers. Boundary edges, signed volume, area before and after, ray-cast checks. A boolean that reports success can still be wrong.

Compose, don’t deform. Build new pieces from a vocabulary of generators. Stretching a finished mesh breaks the proportions its detail depends on.

Pick ornament your tools can carve. Gadroons beat bad acanthus. Choose the version of the design that the procedural method renders convincingly.

Gallery

The hall

Render from the balcony looking down the horseshoe to the stage arch
BalconyThe horseshoe from above, looking to the stage arch.
Render from the stage showing the tiers and the ceiling rose
From the stageTiers and ceiling.
Render from a center box looking at the stage
Centre boxStage arch and columns.
Render from the side showing a stage column and the tiers
SideTier column and boxes.
Render from a box over the velvet rail toward the stage
Box seatOver the velvet rail.
Render of the full horseshoe of tiers
HorseshoeAll five levels.
Render of the ornate ceiling panel above the tiers
CeilingThe rose and dome lights.
Blender viewport showing tiers, seats and a column
ViewportBlender, material preview.
Blender viewport showing tier parapets and box doors
ViewportTier parapets and exit doors.

Tools. Blender 5.2 with the Blender MCP bridge · Houdini 20.5 (hython, SOPs, VEX, Karma) · Python · Cycles with OptiX on an RTX 3090 · Claude Code as a scripting copilot.

Reference. Carnegie Hall’s public seating chart for the Isaac Stern Auditorium / Ronald O. Perelman Stage, a ticketing section map of the hall, and reference photographs of the hall. This is a personal study, not affiliated with Carnegie Hall.

Comments

Tags in this post

  • #technical-art
  • #houdini
  • #blender
  • #python
  • #procedural
  • #environment-art
  • #rendering

Keep reading

  • Rendering Brilliance

    A visual tour of the cubemap-based diamond shader — how a faceted gemstone becomes a single texture lookup, and what that gets you. Eight interactive figures, plain-English asides, and the optics that hold it all together.

    15 min · May 26, 2026

  • Parsing documents without uploading them

    A procurement pipeline that never leaves the tab: PDF, DOCX and XLSX to editable Markdown, PP-OCRv6 on ONNX Runtime Web inside a module worker, and the canvas shims nobody warns you about.

    12 min · Jul 7, 2026

  • Building a 3D ring configurator in Expo

    The complete story of Lumière: loading GLBs on device, composing rings from parts at runtime, a custom refractive gem shader, porting the whole thing to the browser — and the audit that found a stranger's email address inside a binary before I made it public.

    17 min · May 5, 2026

All tags

  • #ai
  • #blender
  • #cubemap
  • #diamond
  • #environment-art
  • #expo
  • #graphics
  • #houdini
  • #huggingface
  • #image-generation
  • #licensing
  • #mdx
  • #meta
  • #next.js
  • #ocr
  • #onnx
  • #open-source
  • #pdf
  • #procedural
  • #python
  • #r3f
  • #ray-tracing
  • #react-native
  • #rendering
  • #replicate
  • #sam2
  • #segmentation
  • #shaders
  • #technical-art
  • #three.js
  • #vercel
  • #wasm
  • #web-worker
  • #webgl
  • #webgpu
← Back to all articles

Tags in this post

  • #technical-art
  • #houdini
  • #blender
  • #python
  • #procedural
  • #environment-art
  • #rendering

Keep reading

  • Rendering Brilliance

    A visual tour of the cubemap-based diamond shader — how a faceted gemstone becomes a single texture lookup, and what that gets you. Eight interactive figures, plain-English asides, and the optics that hold it all together.

    15 min · May 26, 2026

  • Parsing documents without uploading them

    A procurement pipeline that never leaves the tab: PDF, DOCX and XLSX to editable Markdown, PP-OCRv6 on ONNX Runtime Web inside a module worker, and the canvas shims nobody warns you about.

    12 min · Jul 7, 2026

  • Building a 3D ring configurator in Expo

    The complete story of Lumière: loading GLBs on device, composing rings from parts at runtime, a custom refractive gem shader, porting the whole thing to the browser — and the audit that found a stranger's email address inside a binary before I made it public.

    17 min · May 5, 2026

All tags

  • #ai
  • #blender
  • #cubemap
  • #diamond
  • #environment-art
  • #expo
  • #graphics
  • #houdini
  • #huggingface
  • #image-generation
  • #licensing
  • #mdx
  • #meta
  • #next.js
  • #ocr
  • #onnx
  • #open-source
  • #pdf
  • #procedural
  • #python
  • #r3f
  • #ray-tracing
  • #react-native
  • #rendering
  • #replicate
  • #sam2
  • #segmentation
  • #shaders
  • #technical-art
  • #three.js
  • #vercel
  • #wasm
  • #web-worker
  • #webgl
  • #webgpu