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:
+102
-54
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user