Skip to content

Commit c5952b8

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): char the campfire logs, crack the coals and soot the ring
The fire had never been lit: clean bark-brown sticks and a pale concrete-grey ring. A heat field (distance from a core just above the ash bed) now drives the shared shaders. Wood chars to near-black toward the flames; the hottest char shows a Voronoi crack network, noise-masked so only some cracks glow; stones are smoke-blackened on their inner faces. Stone base colours move to a warm dark fieldstone with a wider per-stone tone spread and a stronger pitting bump. The render adds a warm shadow-soft point light in the ember bed. No geometry, slot or budget change. Adds the CampfireLow/TroughLow/TowerLow SELECT rows to scripts/asset_sheet.py so the asset-sheet gate can run on all three round-three pieces. Evidence (live, local .scratch binaries): - blender-5.2.1-windows-x64 (Blender 5.2.1 LTS) and blender-4.5.11-windows-x64 (Blender 4.5.11 LTS): default exit 0; --skip-decimate 9, --stray-vert 15, --lift-z 16, --short-stones 16, --float-logs 17, --gap-courses 18, --uniform-stones 20 on both. - --output renders and passes framing on both (fill y 0.750, margins >= 0.022). - Contact sheet: luma 0.179 / stage 0.169 / warmth +0.093 / sat 0.074. Darker and less saturated than the calibration set (0.237-0.243 luma); the subject is grey stone and char by nature. - Asset sheet (docs/gallery/asset-sheets/campfire.webp): reads as a lit fire pit beside the hydrant, drone, well, wheelbarrow and shelf; not the least-designed object. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 20849f2 commit c5952b8

7 files changed

Lines changed: 111 additions & 7 deletions

File tree

52.7 KB
Loading
4.35 KB
Loading
2.63 KB
Loading

‎scripts/asset_sheet.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
"shipping-crate": (r"^CrateLow$", None),
7878
"signpost": (r"^SignpostLow$", None),
7979
"hitching-post": (r"^HitchPostLow$", None),
80+
"campfire": (r"^CampfireLow$", None),
81+
"water-trough": (r"^TroughLow$", None),
82+
"watchtower": (r"^TowerLow$", None),
8083
"triangulate-tangents": (r"^Buckler(\.Rivets)?$", None),
8184
"car-mirror-symmetry": (r"^(CarBody|Wheel(Front|Rear)|Headlamp|Taillamp|Grille"
8285
r"|DoorMirror|DoorHandle(Front|Rear))$", None),

‎showcase/campfire/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ The stated ring height rose from 0.156 to 0.176 m for the crowns.
9090
upper bound, so the ring keeps the envelope of its widest seeded stone
9191
and only this budget fails (exit 20, spread 0.00000).
9292

93+
**Fire, char and soot (render and shaders only; no budget changed).** The logs were a clean bark brown and the ring a pale concrete grey, so nothing in the still said the fire had ever been lit. A heat field (distance from a core just above the ash bed) now drives three things in the shared shaders: wood chars to near-black toward the flames, the hottest charred wood shows cracked embers (a Voronoi edge network, masked by a coarse noise so only some cracks are live), and stones are smoke-blackened where they face the fire. Stone base colours dropped to a warm dark fieldstone with a wider per-stone tone spread. The render adds a warm point light in the ember bed. Material slots and face counts are unchanged.
94+
9395
Stone is isotropic mottling, speckle and pitting with a seeded tone per
9496
stone (`stone-archway`'s recipe, reading `PieceTone`); sticks and logs
9597
carry their own tone and grain along their axis; the ash has sparse

‎showcase/campfire/campfire.py‎

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -839,10 +839,77 @@ def wood_material(name):
839839
rough.inputs["To Max"].default_value = 0.52
840840
nt.links.new(noise.outputs["Fac"], rough.inputs["Value"])
841841
nt.links.new(rough.outputs["Result"], bsdf.inputs["Roughness"])
842+
# Char: heat falls off with distance from the fire's core (the ash bed at
843+
# the ring centre), so the tripod blackens toward the flames and keeps its
844+
# bark colour at the far ends. Inside the char, cracked embers glow.
845+
heat = _heat_field(nt, coord)
846+
char = nt.nodes.new("ShaderNodeMapRange")
847+
char.inputs["From Min"].default_value = 0.12
848+
char.inputs["From Max"].default_value = 0.55
849+
nt.links.new(heat, char.inputs["Value"])
850+
cmix = nt.nodes.new("ShaderNodeMix")
851+
cmix.data_type = "RGBA"
852+
nt.links.new(char.outputs["Result"], _sock(cmix.inputs, "Factor_Float"))
853+
nt.links.new(_sock(mix.outputs, "Result_Color"), _sock(cmix.inputs, "A_Color"))
854+
_sock(cmix.inputs, "B_Color").default_value = (0.016, 0.013, 0.011, 1.0)
855+
nt.links.new(_sock(cmix.outputs, "Result_Color"), bsdf.inputs["Base Color"])
856+
crack = nt.nodes.new("ShaderNodeTexVoronoi")
857+
crack.feature = "DISTANCE_TO_EDGE"
858+
crack.inputs["Scale"].default_value = 42.0
859+
nt.links.new(coord.outputs["Object"], crack.inputs["Vector"])
860+
seam = nt.nodes.new("ShaderNodeMapRange")
861+
seam.inputs["From Min"].default_value = 0.0
862+
seam.inputs["From Max"].default_value = 0.035
863+
seam.inputs["To Min"].default_value = 1.0
864+
seam.inputs["To Max"].default_value = 0.0
865+
nt.links.new(crack.outputs["Distance"], seam.inputs["Value"])
866+
glow = nt.nodes.new("ShaderNodeMath")
867+
glow.operation = "MULTIPLY"
868+
nt.links.new(seam.outputs["Result"], glow.inputs[0])
869+
hot = nt.nodes.new("ShaderNodeMapRange")
870+
hot.inputs["From Min"].default_value = 0.70
871+
hot.inputs["From Max"].default_value = 0.95
872+
nt.links.new(heat, hot.inputs["Value"])
873+
nt.links.new(hot.outputs["Result"], glow.inputs[1])
874+
# only some of the cracks are live: a coarse noise mask breaks the net
875+
live = nt.nodes.new("ShaderNodeTexNoise")
876+
live.inputs["Scale"].default_value = 9.0
877+
live.inputs["Detail"].default_value = 2.0
878+
nt.links.new(coord.outputs["Object"], live.inputs["Vector"])
879+
lmap = nt.nodes.new("ShaderNodeMapRange")
880+
lmap.inputs["From Min"].default_value = 0.48
881+
lmap.inputs["From Max"].default_value = 0.62
882+
nt.links.new(live.outputs["Fac"], lmap.inputs["Value"])
883+
masked = nt.nodes.new("ShaderNodeMath")
884+
masked.operation = "MULTIPLY"
885+
nt.links.new(glow.outputs["Value"], masked.inputs[0])
886+
nt.links.new(lmap.outputs["Result"], masked.inputs[1])
887+
bsdf.inputs["Emission Color"].default_value = (1.0, 0.30, 0.05, 1.0)
888+
estr = nt.nodes.new("ShaderNodeMath")
889+
estr.operation = "MULTIPLY"
890+
estr.inputs[1].default_value = 3.5
891+
nt.links.new(masked.outputs["Value"], estr.inputs[0])
892+
nt.links.new(estr.outputs["Value"], bsdf.inputs["Emission Strength"])
842893
return mat
843894

844895

845-
def stone_material(name, light, dark, roughness, mottle, speck, bump):
896+
def _heat_field(nt, coord):
897+
"""0..1 heat: 1 at the fire's core just above the ash bed, 0 by ~0.34 m out."""
898+
off = nt.nodes.new("ShaderNodeVectorMath")
899+
off.operation = "SUBTRACT"
900+
off.inputs[1].default_value = (0.0, 0.0, 0.06)
901+
nt.links.new(coord.outputs["Object"], off.inputs[0])
902+
dist = nt.nodes.new("ShaderNodeVectorMath")
903+
dist.operation = "LENGTH"
904+
nt.links.new(off.outputs["Vector"], dist.inputs[0])
905+
heat = nt.nodes.new("ShaderNodeMapRange")
906+
heat.inputs["From Min"].default_value = 0.34
907+
heat.inputs["From Max"].default_value = 0.04
908+
nt.links.new(dist.outputs["Value"], heat.inputs["Value"])
909+
return heat.outputs["Result"]
910+
911+
912+
def stone_material(name, light, dark, roughness, mottle, speck, bump, soot=0.0, spread=0.8):
846913
"""Fieldstone: isotropic mottling, speckle and pitting, a tone per stone.
847914
848915
Copied from stone-archway, reading ``PieceTone`` for the per-stone
@@ -883,8 +950,8 @@ def stone_material(name, light, dark, roughness, mottle, speck, bump):
883950
tone.attribute_name = "PieceTone"
884951
shade = nt.nodes.new("ShaderNodeMath")
885952
shade.operation = "MULTIPLY_ADD"
886-
shade.inputs[1].default_value = 0.8
887-
shade.inputs[2].default_value = 0.6
953+
shade.inputs[1].default_value = spread
954+
shade.inputs[2].default_value = 1.0 - 0.5 * spread
888955
nt.links.new(tone.outputs["Fac"], shade.inputs[0])
889956
mix2 = nt.nodes.new("ShaderNodeMix")
890957
mix2.data_type = "RGBA"
@@ -893,6 +960,29 @@ def stone_material(name, light, dark, roughness, mottle, speck, bump):
893960
nt.links.new(_sock(mix.outputs, "Result_Color"), _sock(mix2.inputs, "A_Color"))
894961
nt.links.new(shade.outputs["Value"], _sock(mix2.inputs, "B_Color"))
895962
nt.links.new(_sock(mix2.outputs, "Result_Color"), bsdf.inputs["Base Color"])
963+
if soot:
964+
# smoke-blackened where the stones face the fire, patchy by mottle
965+
heat = _heat_field(nt, coord)
966+
smap = nt.nodes.new("ShaderNodeMapRange")
967+
smap.inputs["From Min"].default_value = 0.05
968+
smap.inputs["From Max"].default_value = 0.40
969+
smap.inputs["To Max"].default_value = soot
970+
nt.links.new(heat, smap.inputs["Value"])
971+
patch = nt.nodes.new("ShaderNodeMath")
972+
patch.operation = "MULTIPLY"
973+
nt.links.new(smap.outputs["Result"], patch.inputs[0])
974+
nt.links.new(mot.outputs["Fac"], patch.inputs[1])
975+
pmul = nt.nodes.new("ShaderNodeMath")
976+
pmul.operation = "MULTIPLY"
977+
pmul.inputs[1].default_value = 1.6
978+
pmul.use_clamp = True
979+
nt.links.new(patch.outputs["Value"], pmul.inputs[0])
980+
smix = nt.nodes.new("ShaderNodeMix")
981+
smix.data_type = "RGBA"
982+
nt.links.new(pmul.outputs["Value"], _sock(smix.inputs, "Factor_Float"))
983+
nt.links.new(_sock(mix2.outputs, "Result_Color"), _sock(smix.inputs, "A_Color"))
984+
_sock(smix.inputs, "B_Color").default_value = (0.022, 0.019, 0.017, 1.0)
985+
nt.links.new(_sock(smix.outputs, "Result_Color"), bsdf.inputs["Base Color"])
896986
rough = nt.nodes.new("ShaderNodeMapRange")
897987
rough.inputs["To Min"].default_value = roughness - 0.06
898988
rough.inputs["To Max"].default_value = min(1.0, roughness + 0.08)
@@ -932,8 +1022,8 @@ def ash_material(name):
9321022
def campfire_materials():
9331023
"""(stone, wood, ash): shared by the check, the render and inspection."""
9341024
stone = stone_material(
935-
"CampfireStone", (0.44, 0.42, 0.38, 1.0), (0.22, 0.215, 0.20, 1.0),
936-
0.86, mottle=6.0, speck=160.0, bump=0.35,
1025+
"CampfireStone", (0.24, 0.20, 0.155, 1.0), (0.085, 0.07, 0.055, 1.0),
1026+
0.86, mottle=6.0, speck=160.0, bump=0.6, soot=0.85, spread=1.3,
9371027
)
9381028
wood = wood_material("CampfireWood")
9391029
ash = ash_material("CampfireAsh")
@@ -1331,9 +1421,18 @@ def light(name, loc, energy, size, col, rot):
13311421
ob.rotation_euler = tuple(math.radians(a) for a in rot)
13321422
scene.collection.objects.link(ob)
13331423

1334-
light("Key", (-3.6, -5.0, 5.8), 680.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
1335-
light("Fill", (5.0, -3.6, 2.6), 48.0, 8.0, (0.72, 0.82, 1.0), (62, 0, 50))
1424+
light("Key", (-3.6, -5.0, 5.8), 640.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
1425+
light("Fill", (5.0, -3.6, 2.6), 40.0, 8.0, (0.72, 0.82, 1.0), (62, 0, 50))
13361426
light("Wedge", (2.4, 4.2, 4.1), 640.0, 5.5, (1.0, 0.70, 0.40), (-70, 0, 198))
1427+
# the fire itself: a warm, shadow-soft point in the ember bed that lights
1428+
# the tripod's inner faces and the soot-dark inner ring (render only)
1429+
fire = bpy.data.lights.new("FireGlow", "POINT")
1430+
fire.energy = 260.0
1431+
fire.color = (1.0, 0.42, 0.12)
1432+
fire.shadow_soft_size = 0.08
1433+
fob = bpy.data.objects.new("FireGlow", fire)
1434+
fob.location = (0.0, 0.0, 0.07)
1435+
scene.collection.objects.link(fob)
13371436

13381437
bb = world_bbox(low)
13391438
span = max(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2], 0.2)

‎showcase/campfire/preview.webp‎

3.88 KB
Loading

0 commit comments

Comments
 (0)