Tree Generator als zweite Extension im selben Repo

Ein Extension-Repo kann mehrere Addons ausliefern (server-generate listet jedes
Zip in dist/) -> die bereits eingetragene Blender-URL liefert jetzt Rock UND Tree,
ein 'Check for Updates' aktualisiert beide.

- stylized_tree_generator.py: Panel 'Tree Gen', Presets baum/palme/busch/kaktus,
  Wachstums-Stufen (gleicher Seed = dieselbe Baum-Identitaet), Modifier-Apply
- Node-Kern 1:1 aus EcoGame tools/blender_tree_gen.py generiert (keine Abweichung)
- build.ps1 baut jetzt beide Extensions
- Headless getestet: alle 4 Presets, Stufen aufsteigend (504/744/1236/1872), Apply ok

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 10:23:54 +02:00
co-authored by Claude Opus 4.8
parent a8ca7a6453
commit 7406d30d4a
8 changed files with 920 additions and 72 deletions
+102 -54
View File
@@ -20,7 +20,7 @@ import random
import traceback
from bpy.props import (
IntProperty, FloatProperty, EnumProperty, BoolProperty,
IntProperty, FloatProperty, FloatVectorProperty, EnumProperty, BoolProperty,
StringProperty, PointerProperty,
)
from bpy.types import Operator, Panel, PropertyGroup
@@ -749,6 +749,15 @@ class RockGenSettings(PropertyGroup):
bake_normal: BoolProperty(name="Normal", default=True)
bake_roughness: BoolProperty(name="Roughness", default=True)
bake_ao: BoolProperty(name="AO / Cavity", default=True)
bake_base_color: FloatVectorProperty(
name="Grundfarbe", subtype='COLOR', size=4,
default=(0.52, 0.47, 0.40, 1.0), min=0.0, max=1.0,
description="Grundton des Steins; Outline/Highlight werden davon abgeleitet",
)
bake_cell_scale: FloatProperty(
name="Zellgroesse", default=2.5, min=0.5, max=12.0,
description="Kleiner = groessere Zellen (weniger Bruchstuecke)",
)
# --- FBX-Export ---
export_dir: StringProperty(
@@ -937,13 +946,18 @@ class OBJECT_OT_export_rocks_fbx(Operator):
# Textur-Bake (nahtlos tileable via 4D-Torus-Projektion, fuer Triplanar in UE)
# ---------------------------------------------------------------------------
def _build_bake_material():
"""Baut ein prozedurales, nahtlos tileables Stylized-Rock-Material.
def _build_bake_material(base_color=(0.52, 0.47, 0.40), cell_scale=2.5):
"""Baut ein prozedurales, nahtlos tileables Stylized-Rock-Material (Zell-Look).
Trick: Die 2D-UV wird als (cos u, sin u, cos v, sin v) auf einen Torus
projiziert und als 4D-Noise gesampelt -> tilet in U und V ohne Naht.
Liefert (mat, sockets) mit den fertigen Ausgangs-Sockets fuer die Bake-Passes.
projiziert und 4D gesampelt -> tilet in U und V ohne Naht. Voronoi liefert
hand-painted Zellen mit dunklen Outlines; base_color/cell_scale steuern Ton
und Zellgroesse. Liefert (mat, sockets) fuer die Bake-Passes.
"""
br, bg, bb = base_color[0], base_color[1], base_color[2]
def _tone(factor):
return (min(br * factor, 1.0), min(bg * factor, 1.0), min(bb * factor, 1.0), 1.0)
mat = bpy.data.materials.new("__RockBakeMat")
mat.use_nodes = True
nt = mat.node_tree
@@ -974,62 +988,91 @@ def _build_bake_material():
links.new(su, comb.inputs[1])
links.new(cv, comb.inputs[2])
def _torus_noise(scale, detail):
n = nodes.new("ShaderNodeTexNoise")
n.noise_dimensions = '4D'
n.inputs["Scale"].default_value = scale
n.inputs["Detail"].default_value = detail
links.new(comb.outputs[0], n.inputs["Vector"])
links.new(sv, n.inputs["W"])
return n
# Domain-Warp: verzerrt die Zellen leicht organisch (tileable, da gleiche
# Torus-Coords -> ueber die Naht hinweg stetig).
warp_noise = nodes.new("ShaderNodeTexNoise")
warp_noise.noise_dimensions = '4D'
warp_noise.inputs["Scale"].default_value = 3.0
warp_noise.inputs["Detail"].default_value = 2.0
links.new(comb.outputs[0], warp_noise.inputs["Vector"])
links.new(sv, warp_noise.inputs["W"])
warp_scale = nodes.new("ShaderNodeVectorMath"); warp_scale.operation = 'SCALE'
links.new(warp_noise.outputs["Color"], warp_scale.inputs[0])
warp_scale.inputs[3].default_value = 0.15
comb_w = nodes.new("ShaderNodeVectorMath"); comb_w.operation = 'ADD'
links.new(comb.outputs[0], comb_w.inputs[0])
links.new(warp_scale.outputs[0], comb_w.inputs[1])
noise_base = _torus_noise(2.5, 6.0) # grobe Formvariation
noise_cavity = _torus_noise(5.0, 6.0) # feinere Risse/Cavity
noise_detail = _torus_noise(9.0, 8.0) # Mikro-Detail fuer die Normal Map
# Zwei Voronoi-Nodes mit identischen Coords: Rand-Distanz + Per-Zell-Farbe.
vor_edge = nodes.new("ShaderNodeTexVoronoi")
vor_edge.voronoi_dimensions = '4D'
vor_edge.feature = 'DISTANCE_TO_EDGE'
vor_edge.inputs["Scale"].default_value = cell_scale
links.new(comb_w.outputs[0], vor_edge.inputs["Vector"])
links.new(sv, vor_edge.inputs["W"])
# Cavity/AO: Kontrast per MapRange
vor_cell = nodes.new("ShaderNodeTexVoronoi")
vor_cell.voronoi_dimensions = '4D'
vor_cell.feature = 'F1'
vor_cell.inputs["Scale"].default_value = cell_scale
links.new(comb_w.outputs[0], vor_cell.inputs["Vector"])
links.new(sv, vor_cell.inputs["W"])
# cell_fac: 0 an den Zellraendern (dunkle Outlines), 1 in der Zellmitte.
# Breiter Bereich -> jede Zelle bekommt einen weichen Woelbungs-Gradient.
cell_fac = nodes.new("ShaderNodeMapRange")
links.new(vor_edge.outputs["Distance"], cell_fac.inputs["Value"])
cell_fac.inputs["From Min"].default_value = 0.0
cell_fac.inputs["From Max"].default_value = 0.32
cell_fac.inputs["To Min"].default_value = 0.0
cell_fac.inputs["To Max"].default_value = 1.0
cell_fac.clamp = True
# Per-Zell-Helligkeit (leichte Variation Zelle zu Zelle)
cell_bw = nodes.new("ShaderNodeRGBToBW")
links.new(vor_cell.outputs["Color"], cell_bw.inputs[0])
cell_var = nodes.new("ShaderNodeMapRange")
links.new(cell_bw.outputs[0], cell_var.inputs["Value"])
cell_var.inputs["To Min"].default_value = 0.78
cell_var.inputs["To Max"].default_value = 1.08
cell_var.clamp = True
combined = nodes.new("ShaderNodeMath"); combined.operation = 'MULTIPLY'
links.new(cell_fac.outputs[0], combined.inputs[0])
links.new(cell_var.outputs[0], combined.inputs[1])
# BaseColor: dunkle Outline -> Zell-Ton -> Highlight (hand-painted Look),
# alle Toene aus base_color abgeleitet.
ramp = nodes.new("ShaderNodeValToRGB")
cr = ramp.color_ramp
cr.elements[0].position = 0.0
cr.elements[0].color = _tone(0.12) # Riss / Outline
cr.elements[1].position = 0.85
cr.elements[1].color = _tone(1.22) # Zell-Highlight
mid = cr.elements.new(0.32)
mid.color = _tone(0.72) # Zell-Grundton
links.new(combined.outputs[0], ramp.inputs["Fac"])
# Roughness: an den Rissen etwas rauer
rough = nodes.new("ShaderNodeMapRange")
links.new(cell_fac.outputs[0], rough.inputs["Value"])
rough.inputs["To Min"].default_value = 0.95
rough.inputs["To Max"].default_value = 0.72
rough.clamp = True
# AO: Zellraender abgedunkelt
ao = nodes.new("ShaderNodeMapRange")
links.new(noise_cavity.outputs["Fac"], ao.inputs["Value"])
ao.inputs["From Min"].default_value = 0.3
ao.inputs["From Max"].default_value = 0.7
links.new(cell_fac.outputs[0], ao.inputs["Value"])
ao.inputs["To Min"].default_value = 0.35
ao.inputs["To Max"].default_value = 1.0
ao.clamp = True
# BaseColor: kombinierter Fac (Form * Cavity) durch ColorRamp
comb_fac = nodes.new("ShaderNodeMath"); comb_fac.operation = 'MULTIPLY'
links.new(noise_base.outputs["Fac"], comb_fac.inputs[0])
links.new(ao.outputs[0], comb_fac.inputs[1])
ramp = nodes.new("ShaderNodeValToRGB")
ramp.color_ramp.elements[0].position = 0.25
ramp.color_ramp.elements[0].color = (0.09, 0.09, 0.10, 1.0)
ramp.color_ramp.elements[1].position = 0.85
ramp.color_ramp.elements[1].color = (0.46, 0.42, 0.37, 1.0)
links.new(comb_fac.outputs[0], ramp.inputs["Fac"])
# Roughness: rauer in dunklen Bereichen
rough = nodes.new("ShaderNodeMapRange")
links.new(noise_base.outputs["Fac"], rough.inputs["Value"])
rough.inputs["To Min"].default_value = 0.9
rough.inputs["To Max"].default_value = 0.6
rough.clamp = True
# Bump -> Normal (fuer den Normal-Bake). Height = grobe Form + Mikro-Detail,
# damit die Normal Map sichtbare Struktur bekommt (nicht flach).
height_mix = nodes.new("ShaderNodeMath"); height_mix.operation = 'ADD'
height_mix.inputs[1].default_value = 0.0
hb = nodes.new("ShaderNodeMath"); hb.operation = 'MULTIPLY'
hb.inputs[1].default_value = 0.6
links.new(noise_base.outputs["Fac"], hb.inputs[0])
hd = nodes.new("ShaderNodeMath"); hd.operation = 'MULTIPLY'
hd.inputs[1].default_value = 0.4
links.new(noise_detail.outputs["Fac"], hd.inputs[0])
links.new(hb.outputs[0], height_mix.inputs[0])
links.new(hd.outputs[0], height_mix.inputs[1])
# Bump: Risse werden zu Rillen (cell_fac niedrig am Rand -> vertieft)
bump = nodes.new("ShaderNodeBump")
bump.inputs["Strength"].default_value = 0.9
bump.inputs["Distance"].default_value = 0.5
links.new(height_mix.outputs[0], bump.inputs["Height"])
bump.inputs["Strength"].default_value = 0.7
bump.inputs["Distance"].default_value = 0.4
links.new(cell_fac.outputs[0], bump.inputs["Height"])
links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
links.new(rough.outputs[0], bsdf.inputs["Roughness"])
links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
@@ -1092,7 +1135,10 @@ class OBJECT_OT_bake_rock_textures(Operator):
bpy.ops.mesh.primitive_plane_add(size=2.0)
plane = context.active_object
plane.name = "__RockBakePlane"
mat, sk = _build_bake_material()
mat, sk = _build_bake_material(
base_color=tuple(settings.bake_base_color),
cell_scale=settings.bake_cell_scale,
)
plane.data.materials.clear()
plane.data.materials.append(mat)
@@ -1346,6 +1392,8 @@ class VIEW3D_PT_rock_generator(Panel):
box.prop(settings, "bake_dir")
box.prop(settings, "bake_prefix")
box.prop(settings, "bake_resolution")
box.prop(settings, "bake_base_color")
box.prop(settings, "bake_cell_scale")
row = box.row(align=True)
row.prop(settings, "bake_color", toggle=True)
row.prop(settings, "bake_normal", toggle=True)