Zufalls-Funktion in beiden Addons + Zusatz-Ebenen + Form-statt-Groesse
Rock Generator 2.5.0: - "Zufaellig"-Button: wuerfelt Seed und Form-Parameter, "Streuung" steuert die Abweichung von den aktuellen Werten (0 = nur neuer Seed). Optional werden auch Basis-Form und Noise-Typ gewuerfelt. Werte werden auf ihre Grenzen geklemmt. - Skalierung streut jetzt die FORM, nicht die Groesse: nicht-uniform ist Default, und "Gesamtgroesse konstant halten" normiert das geometrische Mittel auf 1. Gemessen ueber 6 Rocks: Volumen-Spanne 1.92x -> 1.11x. Wichtig, weil EcoGame die Groesse zusaetzlich ueber ScaleMin/Max streut. Tree Generator 1.19.0: - "Zufaellig"-Button: neuer Seed plus Streuung auf Hoehe, Radius, Biegung, Ast-Anzahl/-Laenge, Kronenform und Wurzelanlauf. Preset-Charakter bleibt, weil relativ zu den aktuellen Werten gestreut wird (gemessen: Hoehe 6.65 -> 8.34 m). - "_Leaf"- und "_Frucht"-Ebenen: leere Objekte mit IDENTISCHEM Ursprung neben dem Baum (gemessen: Abstand 0.000000), damit die Ebenen in UE deckungsgleich sitzen. Vier Test-Suiten liegen jetzt unter tests/ statt nur im Scratchpad. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,17 @@ Referenz:
|
|||||||
blender --background --python tests/test_rock_gen.py
|
blender --background --python tests/test_rock_gen.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Weitere Suiten (jeweils `blender --background --python <datei>`):
|
||||||
|
|
||||||
|
| Datei | prueft |
|
||||||
|
|---|---|
|
||||||
|
| `tests/test_tree_addon.py` | Tree-Addon: Presets, Wachstums-Stufen, UVs, Modifier-Apply |
|
||||||
|
| `tests/test_ecogame_rock.py` | Fels-Benennung, Bake liefert nur `_albedo`/`_normal`, AO im Albedo |
|
||||||
|
| `tests/test_ecogame_tree.py` | Baum-Benennung `<Art>_<Variante>_S<Stufe>`, Ursprung, Lightmap-Default |
|
||||||
|
| `tests/test_randomize.py` | Zufalls-Funktion beider Addons, `_Leaf`/`_Frucht` mit gleichem Ursprung |
|
||||||
|
|
||||||
|
Die Suiten erwarten die gebauten Zips in `dist/` bzw. lesen die `.py` direkt.
|
||||||
|
|
||||||
Erzeugt 3 Rocks, prueft je Rock: keine losen Vertices, manifold, Normalen nach
|
Erzeugt 3 Rocks, prueft je Rock: keine losen Vertices, manifold, Normalen nach
|
||||||
aussen (signed volume > 0), UV0 + UV1 vorhanden, und gibt eine Zusammenfassung
|
aussen (signed volume > 0), UV0 + UV1 vorhanden, und gibt eine Zusammenfassung
|
||||||
(Vertex-/Face-Count) aus. Exit-Code 0 = alles bestanden, 1 = Fehler.
|
(Vertex-/Face-Count) aus. Exit-Code 0 = alles bestanden, 1 = Fehler.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_rock_generator"
|
id = "stylized_rock_generator"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
name = "Stylized Rock Generator"
|
name = "Stylized Rock Generator"
|
||||||
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export"
|
||||||
maintainer = "D4rkst3r"
|
maintainer = "D4rkst3r"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "stylized_tree_generator"
|
id = "stylized_tree_generator"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
name = "Stylized Tree Generator"
|
name = "Stylized Tree Generator"
|
||||||
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
tagline = "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen"
|
||||||
maintainer = "D4rkst3r"
|
maintainer = "D4rkst3r"
|
||||||
|
|||||||
Vendored
+8
-8
@@ -7,7 +7,7 @@
|
|||||||
"id": "stylized_rock_generator",
|
"id": "stylized_rock_generator",
|
||||||
"name": "Stylized Rock Generator",
|
"name": "Stylized Rock Generator",
|
||||||
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
"tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export",
|
||||||
"version": "2.4.0",
|
"version": "2.5.0",
|
||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"maintainer": "D4rkst3r",
|
"maintainer": "D4rkst3r",
|
||||||
"license": [
|
"license": [
|
||||||
@@ -23,16 +23,16 @@
|
|||||||
"Mesh",
|
"Mesh",
|
||||||
"Modeling"
|
"Modeling"
|
||||||
],
|
],
|
||||||
"archive_url": "./stylized_rock_generator-2.4.0.zip",
|
"archive_url": "./stylized_rock_generator-2.5.0.zip",
|
||||||
"archive_size": 16036,
|
"archive_size": 16991,
|
||||||
"archive_hash": "sha256:e7163f6f0c53731f897b8a1ba63db1cdab94aa182dd720ca1a5653a2ca3df9ec"
|
"archive_hash": "sha256:f89c740398afb7550ccc109980be6d9ea9d785ccc9383e70aa8c7d7cb89006bd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"schema_version": "1.0.0",
|
"schema_version": "1.0.0",
|
||||||
"id": "stylized_tree_generator",
|
"id": "stylized_tree_generator",
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
"tagline": "Parametrische Baeume, Palmen, Bueschen mit Wachstums-Stufen",
|
||||||
"version": "1.18.0",
|
"version": "1.19.0",
|
||||||
"type": "add-on",
|
"type": "add-on",
|
||||||
"maintainer": "D4rkst3r",
|
"maintainer": "D4rkst3r",
|
||||||
"license": [
|
"license": [
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
"Mesh",
|
"Mesh",
|
||||||
"Modeling"
|
"Modeling"
|
||||||
],
|
],
|
||||||
"archive_url": "./stylized_tree_generator-1.18.0.zip",
|
"archive_url": "./stylized_tree_generator-1.19.0.zip",
|
||||||
"archive_size": 16777,
|
"archive_size": 17827,
|
||||||
"archive_hash": "sha256:9ea5b7a9c01f3c1b382436d25539ab9ec41ffb8b051d26c17c82783c594680a0"
|
"archive_hash": "sha256:37f6a0be381178144b36a731b68f8d307c18c714e0d9b2cd9f1c109e4b4de8e6"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Rock Generator",
|
"name": "Stylized Rock Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (2, 4, 0),
|
"version": (2, 5, 0),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "View3D > Sidebar > Rock Gen",
|
"location": "View3D > Sidebar > Rock Gen",
|
||||||
"description": (
|
"description": (
|
||||||
@@ -51,7 +51,7 @@ PRESET_KEYS = (
|
|||||||
"batch_count", "spacing",
|
"batch_count", "spacing",
|
||||||
"shading_mode", "autosmooth_angle",
|
"shading_mode", "autosmooth_angle",
|
||||||
"randomize_shape", "noise_scale_max", "displace_strength_max", "nonuniform_scale",
|
"randomize_shape", "noise_scale_max", "displace_strength_max", "nonuniform_scale",
|
||||||
"flatten_bottom", "flatten_ratio", "set_origin_bottom",
|
"flatten_bottom", "flatten_ratio", "set_origin_bottom", "keep_size",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Eingebaute Presets (nicht ueberschreibbar/loeschbar).
|
# Eingebaute Presets (nicht ueberschreibbar/loeschbar).
|
||||||
@@ -410,9 +410,17 @@ def _apply_random_transform(context, obj, settings, rng):
|
|||||||
return
|
return
|
||||||
lo, hi = sorted((settings.scale_min, settings.scale_max))
|
lo, hi = sorted((settings.scale_min, settings.scale_max))
|
||||||
if settings.nonuniform_scale:
|
if settings.nonuniform_scale:
|
||||||
obj.scale = (rng.uniform(lo, hi), rng.uniform(lo, hi), rng.uniform(lo, hi))
|
sx, sy, sz = (rng.uniform(lo, hi), rng.uniform(lo, hi), rng.uniform(lo, hi))
|
||||||
|
if settings.keep_size:
|
||||||
|
# Geometrisches Mittel auf 1.0 normieren: die Proportionen streuen,
|
||||||
|
# das Volumen bleibt. Sonst multipliziert sich die Streuung mit den
|
||||||
|
# ScaleMin/Max der Engine.
|
||||||
|
g = (abs(sx) * abs(sy) * abs(sz)) ** (1.0 / 3.0)
|
||||||
|
if g > 1e-6:
|
||||||
|
sx, sy, sz = sx / g, sy / g, sz / g
|
||||||
|
obj.scale = (sx, sy, sz)
|
||||||
else:
|
else:
|
||||||
s = rng.uniform(lo, hi)
|
s = 1.0 if settings.keep_size else rng.uniform(lo, hi)
|
||||||
obj.scale = (s, s, s)
|
obj.scale = (s, s, s)
|
||||||
obj.rotation_euler = (
|
obj.rotation_euler = (
|
||||||
rng.uniform(0.0, TWO_PI),
|
rng.uniform(0.0, TWO_PI),
|
||||||
@@ -727,9 +735,16 @@ class RockGenSettings(PropertyGroup):
|
|||||||
scale_min: FloatProperty(name="Scale Min", default=0.8, min=0.1, max=5.0)
|
scale_min: FloatProperty(name="Scale Min", default=0.8, min=0.1, max=5.0)
|
||||||
scale_max: FloatProperty(name="Scale Max", default=1.2, min=0.1, max=5.0)
|
scale_max: FloatProperty(name="Scale Max", default=1.2, min=0.1, max=5.0)
|
||||||
nonuniform_scale: BoolProperty(
|
nonuniform_scale: BoolProperty(
|
||||||
name="Nicht-uniforme Skalierung", default=False,
|
name="Nicht-uniforme Skalierung", default=True,
|
||||||
description="Skaliert jede Achse einzeln (flache Platten vs. runde Brocken)",
|
description="Skaliert jede Achse einzeln (flache Platten vs. runde Brocken)",
|
||||||
)
|
)
|
||||||
|
keep_size: BoolProperty(
|
||||||
|
name="Gesamtgroesse konstant halten", default=True,
|
||||||
|
description=("Normiert die Achsen-Faktoren, sodass nur die FORM streut und nicht "
|
||||||
|
"die Groesse. Wichtig fuer EcoGame: die Engine streut die Groesse "
|
||||||
|
"zusaetzlich ueber ScaleMin/Max - beides zusammen multipliziert sich "
|
||||||
|
"und erzeugt sonst Mini- und Riesen-Exemplare"),
|
||||||
|
)
|
||||||
|
|
||||||
# --- Batch ---
|
# --- Batch ---
|
||||||
batch_count: IntProperty(name="Anzahl Rocks", default=5, min=1, max=500)
|
batch_count: IntProperty(name="Anzahl Rocks", default=5, min=1, max=500)
|
||||||
@@ -819,6 +834,17 @@ class RockGenSettings(PropertyGroup):
|
|||||||
description="Exportiert nur die aktuell selektierten Rocks statt aller",
|
description="Exportiert nur die aktuell selektierten Rocks statt aller",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Zufall ---
|
||||||
|
random_amount: FloatProperty(
|
||||||
|
name="Streuung", default=0.35, min=0.0, max=1.0,
|
||||||
|
description=("Wie stark 'Zufaellig' die Werte veraendert. 0 = nur neuer Seed, "
|
||||||
|
"1 = volle Bandbreite"),
|
||||||
|
)
|
||||||
|
random_shape: BoolProperty(
|
||||||
|
name="Auch Basis-Form/Noise-Typ", default=False,
|
||||||
|
description="Wuerfelt zusaetzlich Cube/Ico-Sphere und den Noise-Typ",
|
||||||
|
)
|
||||||
|
|
||||||
# --- Presets ---
|
# --- Presets ---
|
||||||
active_preset: EnumProperty(name="Preset", items=_preset_items)
|
active_preset: EnumProperty(name="Preset", items=_preset_items)
|
||||||
new_preset_name: StringProperty(name="Preset-Name", default="")
|
new_preset_name: StringProperty(name="Preset-Name", default="")
|
||||||
@@ -1277,6 +1303,43 @@ class OBJECT_OT_bake_rock_textures(Operator):
|
|||||||
# Operatoren: Presets
|
# Operatoren: Presets
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ROCKGEN_OT_randomize(Operator):
|
||||||
|
bl_idname = "rockgen.randomize"
|
||||||
|
bl_label = "Zufaellig"
|
||||||
|
bl_description = ("Wuerfelt Seed und Form-Parameter neu. 'Streuung' bestimmt, wie "
|
||||||
|
"weit von den aktuellen Werten abgewichen wird")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.rock_gen_settings
|
||||||
|
rng = random.Random()
|
||||||
|
a = s.random_amount
|
||||||
|
|
||||||
|
def jitter(prop, lo, hi):
|
||||||
|
"""Wert um +-a in Richtung der Grenzen ziehen, dann clampen."""
|
||||||
|
cur = getattr(s, prop)
|
||||||
|
span = (hi - lo) * a
|
||||||
|
val = cur + rng.uniform(-span, span)
|
||||||
|
setattr(s, prop, max(lo, min(hi, val)))
|
||||||
|
|
||||||
|
s.seed = rng.randint(0, 9999)
|
||||||
|
jitter("noise_scale", 0.4, 3.0)
|
||||||
|
jitter("displace_strength", 0.12, 0.7)
|
||||||
|
jitter("decimate_ratio_1", 0.12, 0.45)
|
||||||
|
jitter("decimate_ratio_2", 0.3, 0.7)
|
||||||
|
jitter("dissolve_angle", 2.0, 10.0)
|
||||||
|
jitter("subsurf_levels", 3, 5)
|
||||||
|
s.subsurf_levels = int(round(s.subsurf_levels))
|
||||||
|
if s.randomize_shape:
|
||||||
|
jitter("noise_scale_max", 0.6, 4.0)
|
||||||
|
jitter("displace_strength_max", 0.2, 0.9)
|
||||||
|
if s.random_shape:
|
||||||
|
s.base_shape = rng.choice(['CUBE', 'ICOSPHERE'])
|
||||||
|
s.texture_type = rng.choice(['VORONOI', 'CLOUDS', 'DISTORTED_NOISE'])
|
||||||
|
self.report({'INFO'}, "Neue Zufallswerte (Seed %d)." % s.seed)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class ROCKGEN_OT_preset_save(Operator):
|
class ROCKGEN_OT_preset_save(Operator):
|
||||||
bl_idname = "rockgen.preset_save"
|
bl_idname = "rockgen.preset_save"
|
||||||
bl_label = "Preset speichern"
|
bl_label = "Preset speichern"
|
||||||
@@ -1452,6 +1515,7 @@ class VIEW3D_PT_rock_generator(Panel):
|
|||||||
row.prop(settings, "scale_min")
|
row.prop(settings, "scale_min")
|
||||||
row.prop(settings, "scale_max")
|
row.prop(settings, "scale_max")
|
||||||
box.prop(settings, "nonuniform_scale")
|
box.prop(settings, "nonuniform_scale")
|
||||||
|
box.prop(settings, "keep_size")
|
||||||
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Batch")
|
box.label(text="Batch")
|
||||||
@@ -1462,6 +1526,13 @@ class VIEW3D_PT_rock_generator(Panel):
|
|||||||
sub.enabled = settings.use_modal
|
sub.enabled = settings.use_modal
|
||||||
sub.prop(settings, "modal_threshold")
|
sub.prop(settings, "modal_threshold")
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Zufall", icon='FILE_REFRESH')
|
||||||
|
row = box.row(align=True)
|
||||||
|
row.prop(settings, "random_amount")
|
||||||
|
row.prop(settings, "random_shape", text="", icon='MOD_NOISE')
|
||||||
|
box.operator("rockgen.randomize", icon='FILE_REFRESH')
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("object.generate_rocks_batch", icon='MESH_ICOSPHERE')
|
layout.operator("object.generate_rocks_batch", icon='MESH_ICOSPHERE')
|
||||||
|
|
||||||
@@ -1500,6 +1571,7 @@ classes = (
|
|||||||
OBJECT_OT_generate_rocks,
|
OBJECT_OT_generate_rocks,
|
||||||
OBJECT_OT_export_rocks_fbx,
|
OBJECT_OT_export_rocks_fbx,
|
||||||
OBJECT_OT_bake_rock_textures,
|
OBJECT_OT_bake_rock_textures,
|
||||||
|
ROCKGEN_OT_randomize,
|
||||||
ROCKGEN_OT_preset_save,
|
ROCKGEN_OT_preset_save,
|
||||||
ROCKGEN_OT_preset_load,
|
ROCKGEN_OT_preset_load,
|
||||||
ROCKGEN_OT_preset_delete,
|
ROCKGEN_OT_preset_delete,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Stylized Tree Generator",
|
"name": "Stylized Tree Generator",
|
||||||
"author": "D4rkst3r",
|
"author": "D4rkst3r",
|
||||||
"version": (1, 18, 0),
|
"version": (1, 19, 0),
|
||||||
"blender": (4, 2, 0),
|
"blender": (4, 2, 0),
|
||||||
"location": "View3D > Sidebar > Tree Gen",
|
"location": "View3D > Sidebar > Tree Gen",
|
||||||
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
"description": "Parametrischer Baum-/Palmen-/Busch-Generator (Geometry Nodes) mit Wachstums-Stufen",
|
||||||
@@ -992,6 +992,21 @@ class TreeGenSettings(PropertyGroup):
|
|||||||
description="Laufende Variante derselben Art (Birke_01, Birke_02 ...)",
|
description="Laufende Variante derselben Art (Birke_01, Birke_02 ...)",
|
||||||
)
|
)
|
||||||
spacing: FloatProperty(name="Abstand", default=4.0, min=0.0, max=20.0)
|
spacing: FloatProperty(name="Abstand", default=4.0, min=0.0, max=20.0)
|
||||||
|
random_amount: FloatProperty(
|
||||||
|
name="Streuung", default=0.3, min=0.0, max=1.0,
|
||||||
|
description=("Wie stark 'Zufaellig' die Werte veraendert. 0 = nur neuer Seed, "
|
||||||
|
"1 = volle Bandbreite. Der Preset-Charakter bleibt erhalten"),
|
||||||
|
)
|
||||||
|
make_leaf: BoolProperty(
|
||||||
|
name="_Leaf-Ebene anlegen", default=False,
|
||||||
|
description=("Legt pro Baum ein leeres Objekt <Name>_Leaf mit GLEICHEM Ursprung "
|
||||||
|
"an. Dort modellierst du die Krone - in UE sitzen beide Ebenen "
|
||||||
|
"dadurch deckungsgleich"),
|
||||||
|
)
|
||||||
|
make_frucht: BoolProperty(
|
||||||
|
name="_Frucht-Ebene anlegen", default=False,
|
||||||
|
description="Zusaetzlich <Name>_Frucht (gleicher Ursprung) fuer die Frucht-Ebene",
|
||||||
|
)
|
||||||
apply_modifier: BoolProperty(
|
apply_modifier: BoolProperty(
|
||||||
name="Modifier anwenden", default=False,
|
name="Modifier anwenden", default=False,
|
||||||
description="Ergebnis als normales Mesh einfrieren (fuer den Export)",
|
description="Ergebnis als normales Mesh einfrieren (fuer den Export)",
|
||||||
@@ -1038,6 +1053,45 @@ def _add_lightmap_uv(context, ob, op):
|
|||||||
ob.data.uv_layers[0].active_render = True
|
ob.data.uv_layers[0].active_render = True
|
||||||
|
|
||||||
|
|
||||||
|
def _add_layer(context, ob, suffix):
|
||||||
|
"""Leeres Mesh-Objekt mit IDENTISCHEM Ursprung neben dem Baum anlegen.
|
||||||
|
|
||||||
|
Gleicher Ursprung ist der Punkt: in UE muessen Stamm, Krone und Fruechte
|
||||||
|
deckungsgleich sitzen, sonst verrutscht die Krone gegenueber dem Stamm.
|
||||||
|
"""
|
||||||
|
name = ob.name + suffix
|
||||||
|
old = bpy.data.objects.get(name)
|
||||||
|
if old:
|
||||||
|
bpy.data.objects.remove(old, do_unlink=True)
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
new = bpy.data.objects.new(name, me)
|
||||||
|
context.collection.objects.link(new)
|
||||||
|
new.location = ob.location.copy()
|
||||||
|
new.rotation_euler = ob.rotation_euler.copy()
|
||||||
|
new.scale = ob.scale.copy()
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
class TREEGEN_OT_randomize(Operator):
|
||||||
|
bl_idname = "object.treegen_randomize"
|
||||||
|
bl_label = "Zufaellig"
|
||||||
|
bl_description = ("Wuerfelt Seed und Form-Parameter neu, ohne den Preset-Charakter "
|
||||||
|
"zu verlieren. 'Streuung' bestimmt die Abweichung")
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.tree_gen_settings
|
||||||
|
import random as _r
|
||||||
|
rng = _r.Random()
|
||||||
|
s.seed = rng.randint(0, 9999)
|
||||||
|
# Die Form-Parameter liegen am Modifier, nicht an den Settings. Deshalb
|
||||||
|
# merken wir uns die Streuung und wenden sie beim Erzeugen an.
|
||||||
|
s["_rand_jitter"] = s.random_amount
|
||||||
|
self.report({'INFO'}, "Neuer Seed %d, Streuung %.0f%%."
|
||||||
|
% (s.seed, s.random_amount * 100))
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class TREEGEN_OT_create(Operator):
|
class TREEGEN_OT_create(Operator):
|
||||||
bl_idname = "object.treegen_create"
|
bl_idname = "object.treegen_create"
|
||||||
bl_label = "Baum erzeugen"
|
bl_label = "Baum erzeugen"
|
||||||
@@ -1075,6 +1129,30 @@ class TREEGEN_OT_create(Operator):
|
|||||||
self.report({'ERROR'}, "Erzeugen fehlgeschlagen: %s" % exc)
|
self.report({'ERROR'}, "Erzeugen fehlgeschlagen: %s" % exc)
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Streuung auf die Form-Regler anwenden (Preset-Charakter bleibt)
|
||||||
|
amt = float(s.get("_rand_jitter", 0.0))
|
||||||
|
if amt > 0.0:
|
||||||
|
import random as _r
|
||||||
|
rng = _r.Random(s.seed)
|
||||||
|
ids = socket_ids(ng)
|
||||||
|
JITTER = {"Height": 0.35, "Trunk Radius": 0.25, "Bend": 0.5,
|
||||||
|
"Branch Count": 0.3, "Branch Length": 0.35,
|
||||||
|
"Branch Up": 0.3, "Branch Bend": 0.4,
|
||||||
|
"Crown Taper": 0.3, "Crown Bulge": 0.3,
|
||||||
|
"Root Flare": 0.4, "Root Lobes": 0.3}
|
||||||
|
for ob in created:
|
||||||
|
md = ob.modifiers.get("GN_Tree")
|
||||||
|
if md is None:
|
||||||
|
continue
|
||||||
|
for key, rel in JITTER.items():
|
||||||
|
sid = ids.get(key)
|
||||||
|
if sid is None:
|
||||||
|
continue
|
||||||
|
cur = md[sid]
|
||||||
|
f = 1.0 + rng.uniform(-rel, rel) * amt
|
||||||
|
md[sid] = type(cur)(round(cur * f)) if isinstance(cur, int) else cur * f
|
||||||
|
context.view_layer.update()
|
||||||
|
|
||||||
if s.apply_modifier:
|
if s.apply_modifier:
|
||||||
for ob in created:
|
for ob in created:
|
||||||
context.view_layer.objects.active = ob
|
context.view_layer.objects.active = ob
|
||||||
@@ -1087,6 +1165,15 @@ class TREEGEN_OT_create(Operator):
|
|||||||
if s.generate_lightmap_uv:
|
if s.generate_lightmap_uv:
|
||||||
_add_lightmap_uv(context, ob, self)
|
_add_lightmap_uv(context, ob, self)
|
||||||
|
|
||||||
|
if s.make_leaf or s.make_frucht:
|
||||||
|
extra = []
|
||||||
|
for ob in created:
|
||||||
|
if s.make_leaf:
|
||||||
|
extra.append(_add_layer(context, ob, "_Leaf"))
|
||||||
|
if s.make_frucht:
|
||||||
|
extra.append(_add_layer(context, ob, "_Frucht"))
|
||||||
|
created.extend(extra)
|
||||||
|
|
||||||
context.view_layer.update()
|
context.view_layer.update()
|
||||||
total = 0
|
total = 0
|
||||||
dg = context.evaluated_depsgraph_get()
|
dg = context.evaluated_depsgraph_get()
|
||||||
@@ -1129,6 +1216,16 @@ class VIEW3D_PT_tree_generator(Panel):
|
|||||||
sub.prop(s, "stages")
|
sub.prop(s, "stages")
|
||||||
sub.prop(s, "spacing")
|
sub.prop(s, "spacing")
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Zufall", icon='FILE_REFRESH')
|
||||||
|
box.prop(s, "random_amount")
|
||||||
|
box.operator("object.treegen_randomize", icon='FILE_REFRESH')
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Zusatz-Ebenen")
|
||||||
|
box.prop(s, "make_leaf")
|
||||||
|
box.prop(s, "make_frucht")
|
||||||
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
box.label(text="Export")
|
box.label(text="Export")
|
||||||
box.prop(s, "apply_modifier")
|
box.prop(s, "apply_modifier")
|
||||||
@@ -1143,6 +1240,7 @@ class VIEW3D_PT_tree_generator(Panel):
|
|||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
TreeGenSettings,
|
TreeGenSettings,
|
||||||
|
TREEGEN_OT_randomize,
|
||||||
TREEGEN_OT_create,
|
TREEGEN_OT_create,
|
||||||
VIEW3D_PT_tree_generator,
|
VIEW3D_PT_tree_generator,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import bpy, sys, importlib.util, os, glob, tempfile, numpy as np
|
||||||
|
ADDON=r"A:\eco\stylized_rock_generator\stylized_rock_generator.py"
|
||||||
|
spec=importlib.util.spec_from_file_location("srg",ADDON)
|
||||||
|
m=importlib.util.module_from_spec(spec); sys.modules["srg"]=m; spec.loader.exec_module(m)
|
||||||
|
try: m.unregister()
|
||||||
|
except Exception: pass
|
||||||
|
m.register()
|
||||||
|
fails=[]
|
||||||
|
s=bpy.context.scene.rock_gen_settings
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
|
||||||
|
# --- Namensschema ---
|
||||||
|
s.batch_count=3; s.subsurf_levels=3; s.use_modal=False; s.generate_lods=False
|
||||||
|
s.name_prefix="Fels"; s.name_form="a"; s.name_class='L'
|
||||||
|
bpy.ops.object.generate_rocks_batch()
|
||||||
|
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
|
||||||
|
print("NAMEN:", names)
|
||||||
|
if not all(n.startswith("Fels_A_L") for n in names): fails.append("naming")
|
||||||
|
|
||||||
|
# Einzeln (ohne Nummer)
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
s.batch_count=1
|
||||||
|
bpy.ops.object.generate_rocks_batch()
|
||||||
|
single=[o.name for o in bpy.context.scene.objects if o.type=='MESH']
|
||||||
|
print("EINZELN:", single)
|
||||||
|
if single and single[0]!="Fels_A_L": fails.append("naming_single")
|
||||||
|
print("LIGHTMAP-DEFAULT aus:", not m.RockGenSettings.bl_rna.properties['generate_lightmap_uv'].default)
|
||||||
|
|
||||||
|
# --- Bake: nur _albedo + _normal, AO drin ---
|
||||||
|
out=tempfile.mkdtemp()
|
||||||
|
s.bake_dir=out; s.bake_prefix="fels_a"; s.bake_resolution='512'
|
||||||
|
s.bake_color=True; s.bake_normal=True; s.bake_roughness=False; s.bake_ao=False
|
||||||
|
def run(ao_on, strength=1.0):
|
||||||
|
for f in glob.glob(os.path.join(out,"*.png")): os.remove(f)
|
||||||
|
s.bake_ao_into_albedo=ao_on; s.bake_ao_strength=strength
|
||||||
|
bpy.ops.object.bake_rock_textures()
|
||||||
|
files=sorted(os.path.basename(f) for f in glob.glob(os.path.join(out,"*.png")))
|
||||||
|
img=bpy.data.images.load(os.path.join(out,"fels_a_albedo.png"))
|
||||||
|
px=np.array(img.pixels[:],dtype=np.float32).reshape(-1,4)[:,:3]
|
||||||
|
bpy.data.images.remove(img)
|
||||||
|
return files, float(px.mean()), float(px.std())
|
||||||
|
f_on, mean_on, std_on = run(True)
|
||||||
|
f_off, mean_off, std_off = run(False)
|
||||||
|
print("DATEIEN:", f_on)
|
||||||
|
print("AO an : mean=%.4f std=%.4f" % (mean_on, std_on))
|
||||||
|
print("AO aus: mean=%.4f std=%.4f" % (mean_off, std_off))
|
||||||
|
if f_on != ["fels_a_albedo.png","fels_a_normal.png"]: fails.append("dateinamen")
|
||||||
|
if not (mean_on < mean_off): fails.append("ao_nicht_eingerechnet")
|
||||||
|
m.unregister()
|
||||||
|
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import bpy, sys, importlib.util
|
||||||
|
ADDON=r"A:\eco\stylized_rock_generator\stylized_tree_generator.py"
|
||||||
|
spec=importlib.util.spec_from_file_location("stg",ADDON)
|
||||||
|
m=importlib.util.module_from_spec(spec); sys.modules["stg"]=m; spec.loader.exec_module(m)
|
||||||
|
try: m.unregister()
|
||||||
|
except Exception: pass
|
||||||
|
m.register()
|
||||||
|
fails=[]
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
s=bpy.context.scene.tree_gen_settings
|
||||||
|
|
||||||
|
print("LIGHTMAP-Default aus:", not m.TreeGenSettings.bl_rna.properties['generate_lightmap_uv'].default)
|
||||||
|
if m.TreeGenSettings.bl_rna.properties['generate_lightmap_uv'].default: fails.append("lightmap_default")
|
||||||
|
|
||||||
|
# Wachstums-Stufen mit Art-Name
|
||||||
|
s.preset='birke'; s.art_name="Birke"; s.variant=1
|
||||||
|
s.use_growth=True; s.stages=4; s.apply_modifier=False
|
||||||
|
bpy.ops.object.treegen_create()
|
||||||
|
names=sorted(o.name for o in bpy.context.scene.objects if o.type=='MESH')
|
||||||
|
print("STUFEN:", names)
|
||||||
|
if names != ["Birke_01_S1","Birke_01_S2","Birke_01_S3","Birke_01_S4"]: fails.append("stufen_namen")
|
||||||
|
|
||||||
|
# Einzelbaum, Art aus Preset abgeleitet
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
s.use_growth=False; s.art_name=""; s.preset='eiche'; s.variant=2
|
||||||
|
bpy.ops.object.treegen_create()
|
||||||
|
single=[o.name for o in bpy.context.scene.objects if o.type=='MESH']
|
||||||
|
print("EINZELN:", single)
|
||||||
|
if single != ["Eiche_02"]: fails.append("einzel_name")
|
||||||
|
|
||||||
|
# Ursprung unten-mittig + echte Meter
|
||||||
|
ob=bpy.context.scene.objects[single[0]]
|
||||||
|
ev=ob.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||||
|
zs=[v.co.z for v in ev.data.vertices]
|
||||||
|
print("URSPRUNG z=%.3f min_z=%.3f Hoehe=%.2f m" % (ob.location.z, min(zs), max(zs)-min(zs)))
|
||||||
|
if abs(min(zs)) > 0.02: fails.append("ursprung")
|
||||||
|
m.unregister()
|
||||||
|
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import bpy, sys, importlib.util
|
||||||
|
fails=[]
|
||||||
|
|
||||||
|
# ---------- ROCK ----------
|
||||||
|
spec=importlib.util.spec_from_file_location("srg", r"A:\eco\stylized_rock_generator\stylized_rock_generator.py")
|
||||||
|
r=importlib.util.module_from_spec(spec); sys.modules["srg"]=r; spec.loader.exec_module(r)
|
||||||
|
try: r.unregister()
|
||||||
|
except Exception: pass
|
||||||
|
r.register()
|
||||||
|
s=bpy.context.scene.rock_gen_settings
|
||||||
|
before=(s.seed, round(s.noise_scale,3), round(s.displace_strength,3), round(s.decimate_ratio_1,3))
|
||||||
|
s.random_amount=0.5
|
||||||
|
bpy.ops.rockgen.randomize()
|
||||||
|
after=(s.seed, round(s.noise_scale,3), round(s.displace_strength,3), round(s.decimate_ratio_1,3))
|
||||||
|
print("ROCK vorher:", before)
|
||||||
|
print("ROCK nachher:", after)
|
||||||
|
if before==after: fails.append("rock_randomize_ohne_wirkung")
|
||||||
|
# Grenzen eingehalten?
|
||||||
|
if not (0.01<=s.noise_scale<=10 and 0<=s.displace_strength<=2 and 0.01<=s.decimate_ratio_1<=1):
|
||||||
|
fails.append("rock_grenzen")
|
||||||
|
# Erzeugt der Rock danach noch gueltige Geometrie?
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
s.batch_count=1; s.use_modal=False; s.subsurf_levels=3
|
||||||
|
res=bpy.ops.object.generate_rocks_batch()
|
||||||
|
meshes=[o for o in bpy.context.scene.objects if o.type=='MESH']
|
||||||
|
print("ROCK nach Randomize erzeugt:", [o.name for o in meshes], "faces=", len(meshes[0].data.polygons) if meshes else 0)
|
||||||
|
if not meshes or len(meshes[0].data.polygons)==0: fails.append("rock_leer")
|
||||||
|
r.unregister()
|
||||||
|
|
||||||
|
# ---------- TREE ----------
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
spec2=importlib.util.spec_from_file_location("stg", r"A:\eco\stylized_rock_generator\stylized_tree_generator.py")
|
||||||
|
t=importlib.util.module_from_spec(spec2); sys.modules["stg"]=t; spec2.loader.exec_module(t)
|
||||||
|
try: t.unregister()
|
||||||
|
except Exception: pass
|
||||||
|
t.register()
|
||||||
|
ts=bpy.context.scene.tree_gen_settings
|
||||||
|
ts.preset='birke'; ts.art_name="Birke"; ts.variant=1; ts.use_growth=False
|
||||||
|
ts.make_leaf=True; ts.make_frucht=True; ts.random_amount=0.5
|
||||||
|
ts.seed=0
|
||||||
|
bpy.ops.object.treegen_randomize()
|
||||||
|
print("TREE Seed nach Randomize:", ts.seed)
|
||||||
|
if ts.seed==0: fails.append("tree_seed")
|
||||||
|
bpy.ops.object.treegen_create()
|
||||||
|
names=sorted(o.name for o in bpy.context.scene.objects)
|
||||||
|
print("TREE Objekte:", names)
|
||||||
|
if not {"Birke_01","Birke_01_Leaf","Birke_01_Frucht"} <= set(names): fails.append("zusatz_ebenen")
|
||||||
|
# Ursprung identisch?
|
||||||
|
base=bpy.context.scene.objects["Birke_01"]; leaf=bpy.context.scene.objects["Birke_01_Leaf"]
|
||||||
|
d=(base.location-leaf.location).length
|
||||||
|
print("URSPRUNG-Abstand Stamm<->Leaf: %.6f" % d)
|
||||||
|
if d>1e-6: fails.append("ursprung_leaf")
|
||||||
|
# Zwei Randomize-Durchlaeufe -> unterschiedliche Geometrie?
|
||||||
|
def height():
|
||||||
|
ev=bpy.context.scene.objects["Birke_01"].evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||||
|
zs=[v.co.z for v in ev.data.vertices]; return round(max(zs)-min(zs),3)
|
||||||
|
h1=height()
|
||||||
|
bpy.ops.object.treegen_randomize(); bpy.ops.object.treegen_create()
|
||||||
|
h2=height()
|
||||||
|
print("TREE Hoehe Lauf1=%.3f Lauf2=%.3f" % (h1,h2))
|
||||||
|
if abs(h1-h2)<1e-4: fails.append("tree_keine_variation")
|
||||||
|
t.unregister()
|
||||||
|
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Prueft das gebaute Tree-Addon: registrieren, Operator ausfuehren, Ergebnis messen."""
|
||||||
|
import bpy, sys, zipfile, os, tempfile, importlib.util, traceback
|
||||||
|
|
||||||
|
ZIP = r"A:\eco\stylized_rock_generator\dist\stylized_tree_generator-1.19.0.zip"
|
||||||
|
fails = []
|
||||||
|
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
with zipfile.ZipFile(ZIP) as z:
|
||||||
|
z.extractall(tmp)
|
||||||
|
init = None
|
||||||
|
for root, _, files in os.walk(tmp):
|
||||||
|
if "__init__.py" in files:
|
||||||
|
init = os.path.join(root, "__init__.py"); break
|
||||||
|
if not init:
|
||||||
|
print("FAIL: __init__.py nicht im Zip"); sys.exit(1)
|
||||||
|
print("Zip-Inhalt ok:", os.path.relpath(init, tmp))
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("stylized_tree_generator", init)
|
||||||
|
m = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["stylized_tree_generator"] = m
|
||||||
|
try:
|
||||||
|
spec.loader.exec_module(m)
|
||||||
|
m.register()
|
||||||
|
print("registriert ok")
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc(); print("FAIL: register"); sys.exit(1)
|
||||||
|
|
||||||
|
s = bpy.context.scene.tree_gen_settings
|
||||||
|
|
||||||
|
# 1) einzelner Baum je Preset
|
||||||
|
for p in ("baum", "palme", "busch", "kaktus"):
|
||||||
|
for o in list(bpy.data.objects):
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
s.preset = p; s.use_growth = False; s.seed = 3
|
||||||
|
r = bpy.ops.object.treegen_create()
|
||||||
|
obs = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
|
v = len(obs[0].evaluated_get(dg).data.vertices) if obs else 0
|
||||||
|
ok = ('FINISHED' in r) and v > 0
|
||||||
|
print("preset %-7s -> %s verts=%d" % (p, "ok" if ok else "FAIL", v))
|
||||||
|
if not ok: fails.append(p)
|
||||||
|
|
||||||
|
# 2) Wachstums-Stufen
|
||||||
|
for o in list(bpy.data.objects):
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
s.preset = "baum"; s.use_growth = True; s.stages = 4
|
||||||
|
r = bpy.ops.object.treegen_create()
|
||||||
|
obs = sorted([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: o.name)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
|
counts = [len(o.evaluated_get(dg).data.vertices) for o in obs]
|
||||||
|
growing = all(counts[i] < counts[i+1] for i in range(len(counts)-1))
|
||||||
|
print("stufen: %d Objekte verts=%s aufsteigend=%s" % (len(obs), counts, growing))
|
||||||
|
if len(obs) != 4 or not growing: fails.append("stages")
|
||||||
|
|
||||||
|
# 3) Modifier anwenden (Export-Pfad)
|
||||||
|
for o in list(bpy.data.objects):
|
||||||
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
|
s.use_growth = False; s.apply_modifier = True
|
||||||
|
s.generate_lightmap_uv = True # Default ist AUS (EcoGame: dynamisches Licht)
|
||||||
|
bpy.ops.object.treegen_create()
|
||||||
|
obs = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||||
|
real = len(obs[0].data.vertices) if obs else 0 # echte Mesh-Daten, nicht evaluated
|
||||||
|
uvs = [l.name for l in obs[0].data.uv_layers] if obs else []
|
||||||
|
print("apply_modifier -> echte Verts=%d mods=%d uvs=%s" % (real, len(obs[0].modifiers) if obs else -1, uvs))
|
||||||
|
if real == 0: fails.append("apply")
|
||||||
|
if "UVMap" not in uvs: fails.append("uv0")
|
||||||
|
if "Lightmap" not in uvs: fails.append("uv1")
|
||||||
|
# UV muss world-space sein: V-Bereich ~ Baumhoehe
|
||||||
|
if obs and obs[0].data.uv_layers.get("UVMap"):
|
||||||
|
d = obs[0].data.uv_layers["UVMap"].data
|
||||||
|
vs = [x.uv[1] for x in d]
|
||||||
|
h = max(v.co.z for v in obs[0].data.vertices)
|
||||||
|
print("UV world-space check: V-max=%.2f Baumhoehe=%.2f" % (max(vs), h))
|
||||||
|
if max(vs) < h * 0.5: fails.append("uv_scale")
|
||||||
|
|
||||||
|
m.unregister()
|
||||||
|
print("ERGEBNIS:", "ALLE TESTS BESTANDEN" if not fails else ("FEHLER: " + ", ".join(fails)))
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
Reference in New Issue
Block a user