Neu: Stylized Grass Generator 1.0.0 (drittes Addon im Repo)
Gras-, Blumen- und Farn-Bueschel. Kern ist die WIND-UV nach Vorgabe des Users:
UV0.V = 0 an der Wurzel, 1 an der Spitze - genau die Maske, die UEs
"SimpleGrassWind" als WindWeight braucht.
Am Zielmaterial geprueft (M_Grass.uasset): es nutzt SimpleGrassWind +
WorldPositionOffset + TextureCoordinate, aber KEINE VertexColor. Die Windmaske
kommt deshalb primaer ueber die UV - die Assets laufen damit ohne
Materialaenderung.
Vertex-Farben zusaetzlich, nicht als einzige Quelle:
R = Kopie der Windmaske (Fallback)
G = Variation je HALM - Ebene unter dem Biom-Tint, den die Engine bereits ueber
PerInstanceCustomData 0/1 pro Instanz setzt; im Material multiplizieren
B = AO (unten dunkel)
Presets gras / gras_hoch / steppengras / farn / blume, Zufalls-Button,
Tri-Budget-Warnung (400 laut ASSETS.md).
Gemessen: UV.V Wurzel 0.000 -> Spitze 1.000, 112 Tris bei 14 Halmen, G hat
exakt 14 verschiedene Werte bei 14 Halmen (also wirklich pro Halm, nicht pro
Vertex), min_z = 0.0000, UV und VCol ueberleben den FBX-Roundtrip.
build.ps1 baut jetzt drei Extensions - dieselbe URL liefert alle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
> |---|---|---|
|
> |---|---|---|
|
||||||
> | **Stylized Rock Generator** | *Rock Gen* | Batch-Rocks + Textur-Bake + FBX/LOD-Export |
|
> | **Stylized Rock Generator** | *Rock Gen* | Batch-Rocks + Textur-Bake + FBX/LOD-Export |
|
||||||
> | **Stylized Tree Generator** | *Tree Gen* | Baeume/Palmen/Bueschen/Kakteen (Geometry Nodes) mit Wachstums-Stufen |
|
> | **Stylized Tree Generator** | *Tree Gen* | Baeume/Palmen/Bueschen/Kakteen (Geometry Nodes) mit Wachstums-Stufen |
|
||||||
|
> | **Stylized Grass Generator** | *Grass Gen* | Gras/Blumen/Farne mit Wind-UV fuer `SimpleGrassWind` |
|
||||||
>
|
>
|
||||||
> Ein Blender-Extension-Repo kann mehrere Extensions ausliefern — `server-generate`
|
> Ein Blender-Extension-Repo kann mehrere Extensions ausliefern — `server-generate`
|
||||||
> listet jedes Zip in `dist/`. Ein „Check for Updates" aktualisiert also beide.
|
> listet jedes Zip in `dist/`. Ein „Check for Updates" aktualisiert also beide.
|
||||||
@@ -79,6 +80,29 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Stylized Grass Generator
|
||||||
|
|
||||||
|
Gras-, Blumen- und Farn-Bueschel. Der Kern ist die **Wind-UV**:
|
||||||
|
|
||||||
|
- **UV0.V = 0 an der Wurzel, 1 an der Spitze** — genau die Maske, die UEs
|
||||||
|
`SimpleGrassWind` als *WindWeight* braucht. Ohne sie rutscht der Halm beim
|
||||||
|
Wehen vom Boden weg oder bewegt sich gar nicht.
|
||||||
|
Am Zielmaterial `M_Grass` geprueft: es liest `TextureCoordinate`, aber **keine**
|
||||||
|
`VertexColor` — deshalb liefert der Generator die Maske PRIMAER ueber die UV.
|
||||||
|
Die Assets funktionieren damit **ohne Materialaenderung**.
|
||||||
|
- **Vertex-Farben zusaetzlich** (nicht als einzige Quelle):
|
||||||
|
`R` = Kopie der Windmaske · `G` = Variation je HALM · `B` = AO (unten dunkel).
|
||||||
|
`G` ist die Ebene **unter** dem Biom-Tint, den die Engine ueber
|
||||||
|
`PerInstanceCustomData 0/1` pro Instanz setzt — im Material **multiplizieren**,
|
||||||
|
nicht ersetzen.
|
||||||
|
- Presets: `gras`, `gras_hoch`, `steppengras`, `farn`, `blume`.
|
||||||
|
- Tri-Budget-Warnung (Default 400 laut `ASSETS.md` fuer Gras/Blume).
|
||||||
|
Gemessen: ein Standard-Bueschel mit 14 Halmen = **112 Tris**.
|
||||||
|
- Ursprung unten-mittig (`min_z = 0.0000`), UV und Vertex-Farben ueberleben den
|
||||||
|
FBX-Roundtrip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Stylized Tree Generator
|
## Stylized Tree Generator
|
||||||
|
|
||||||
Parametrischer **Geaest**-Generator (Stamm + Aeste + Sub-Aeste) als Geometry-Nodes-
|
Parametrischer **Geaest**-Generator (Stamm + Aeste + Sub-Aeste) als Geometry-Nodes-
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "stylized_grass_generator"
|
||||||
|
version = "1.0.0"
|
||||||
|
name = "Stylized Grass Generator"
|
||||||
|
tagline = "Gras, Blumen und Farne mit Wind-UV fuer SimpleGrassWind"
|
||||||
|
maintainer = "D4rkst3r"
|
||||||
|
type = "add-on"
|
||||||
|
|
||||||
|
website = "https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator"
|
||||||
|
|
||||||
|
tags = ["Object", "Mesh", "Modeling"]
|
||||||
|
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
|
||||||
|
license = [
|
||||||
|
"SPDX:GPL-3.0-or-later",
|
||||||
|
]
|
||||||
|
|
||||||
|
copyright = [
|
||||||
|
"2026 D4rkst3r",
|
||||||
|
]
|
||||||
@@ -26,7 +26,8 @@ New-Item -ItemType Directory -Force -Path $dist | Out-Null
|
|||||||
# name = Staging-Ordner, manifest = Quelldatei, source = Addon-Code
|
# name = Staging-Ordner, manifest = Quelldatei, source = Addon-Code
|
||||||
$extensions = @(
|
$extensions = @(
|
||||||
@{ name = "rock"; manifest = "blender_manifest.toml"; source = "stylized_rock_generator.py" },
|
@{ name = "rock"; manifest = "blender_manifest.toml"; source = "stylized_rock_generator.py" },
|
||||||
@{ name = "tree"; manifest = "blender_manifest_tree.toml"; source = "stylized_tree_generator.py" }
|
@{ name = "tree"; manifest = "blender_manifest_tree.toml"; source = "stylized_tree_generator.py" },
|
||||||
|
@{ name = "grass"; manifest = "blender_manifest_grass.toml"; source = "stylized_grass_generator.py" }
|
||||||
)
|
)
|
||||||
|
|
||||||
# Alte Zips weg, damit geloeschte Versionen nicht im Listing haengen bleiben.
|
# Alte Zips weg, damit geloeschte Versionen nicht im Listing haengen bleiben.
|
||||||
|
|||||||
Vendored
+25
@@ -2,6 +2,31 @@
|
|||||||
"version": "v1",
|
"version": "v1",
|
||||||
"blocklist": [],
|
"blocklist": [],
|
||||||
"data": [
|
"data": [
|
||||||
|
{
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"id": "stylized_grass_generator",
|
||||||
|
"name": "Stylized Grass Generator",
|
||||||
|
"tagline": "Gras, Blumen und Farne mit Wind-UV fuer SimpleGrassWind",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "add-on",
|
||||||
|
"maintainer": "D4rkst3r",
|
||||||
|
"license": [
|
||||||
|
"SPDX:GPL-3.0-or-later"
|
||||||
|
],
|
||||||
|
"blender_version_min": "4.2.0",
|
||||||
|
"website": "https://git.d4rkst3r.de/D4rkst3r/stylized-rock-generator",
|
||||||
|
"copyright": [
|
||||||
|
"2026 D4rkst3r"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Object",
|
||||||
|
"Mesh",
|
||||||
|
"Modeling"
|
||||||
|
],
|
||||||
|
"archive_url": "./stylized_grass_generator-1.0.0.zip",
|
||||||
|
"archive_size": 4752,
|
||||||
|
"archive_hash": "sha256:ed8d7dda2f090f93c4ecad3b81af46c007578701e0851035366404399d13e14f"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"schema_version": "1.0.0",
|
"schema_version": "1.0.0",
|
||||||
"id": "stylized_rock_generator",
|
"id": "stylized_rock_generator",
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -0,0 +1,347 @@
|
|||||||
|
bl_info = {
|
||||||
|
"name": "Stylized Grass Generator",
|
||||||
|
"author": "D4rkst3r",
|
||||||
|
"version": (1, 0, 0),
|
||||||
|
"blender": (4, 2, 0),
|
||||||
|
"location": "View3D > Sidebar > Grass Gen",
|
||||||
|
"description": (
|
||||||
|
"Gras-, Blumen- und Farn-Bueschel mit sauberer Wind-UV "
|
||||||
|
"(V = 0 an der Wurzel, 1 an der Spitze) fuer SimpleGrassWind."
|
||||||
|
),
|
||||||
|
"category": "Add Mesh",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# WARUM DIE UV DER KERN IST
|
||||||
|
#
|
||||||
|
# Das Zielmaterial (EcoGame M_Grass) nutzt UEs "SimpleGrassWind" mit
|
||||||
|
# WorldPositionOffset. Dessen WindWeight-Eingang braucht eine Maske, die an der
|
||||||
|
# WURZEL 0 und an der SPITZE 1 ist - sonst rutscht der Halm beim Wehen vom Boden
|
||||||
|
# weg oder bewegt sich gar nicht. Geprueft am Material: es liest
|
||||||
|
# TextureCoordinate, aber KEINE VertexColor. Deshalb liefert dieser Generator
|
||||||
|
# die Windmaske PRIMAER ueber UV0.V.
|
||||||
|
#
|
||||||
|
# Vertex-Farben kommen zusaetzlich (nicht als einzige Quelle):
|
||||||
|
# R = Kopie der Windmaske (Fallback / andere Materialien)
|
||||||
|
# G = Variation je HALM - Ebene UNTER dem Biom-Tint, den die Engine bereits
|
||||||
|
# ueber PerInstanceCustomData 0/1 pro Instanz setzt. Im Material
|
||||||
|
# multiplizieren, nicht ersetzen.
|
||||||
|
# B = AO (unten dunkel)
|
||||||
|
# A = 1 (Reserve)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
from bpy.props import (
|
||||||
|
IntProperty, FloatProperty, EnumProperty, BoolProperty, StringProperty,
|
||||||
|
FloatVectorProperty, PointerProperty,
|
||||||
|
)
|
||||||
|
from bpy.types import Operator, Panel, PropertyGroup
|
||||||
|
|
||||||
|
|
||||||
|
PRESETS = {
|
||||||
|
"gras": dict(blades=14, height=0.42, width=0.030, segments=4,
|
||||||
|
bend=0.42, spread=0.16, tilt=0.55, taper=0.85,
|
||||||
|
curl=0.0, height_var=0.45),
|
||||||
|
"gras_hoch": dict(blades=11, height=0.85, width=0.026, segments=5,
|
||||||
|
bend=0.60, spread=0.20, tilt=0.45, taper=0.90,
|
||||||
|
curl=0.0, height_var=0.40),
|
||||||
|
"steppengras": dict(blades=16, height=0.55, width=0.022, segments=4,
|
||||||
|
bend=0.75, spread=0.26, tilt=0.75, taper=0.95,
|
||||||
|
curl=0.0, height_var=0.55),
|
||||||
|
"farn": dict(blades=7, height=0.50, width=0.075, segments=5,
|
||||||
|
bend=0.70, spread=0.30, tilt=0.85, taper=0.55,
|
||||||
|
curl=0.25, height_var=0.30),
|
||||||
|
"blume": dict(blades=9, height=0.30, width=0.028, segments=3,
|
||||||
|
bend=0.25, spread=0.12, tilt=0.35, taper=0.80,
|
||||||
|
curl=0.0, height_var=0.35),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GrassGenError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Geometrie
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _add_blade(bm, uv_layer, col_layer, cfg, rng):
|
||||||
|
"""Einen Halm als Streifen bauen - inkl. UV und Vertex-Farben.
|
||||||
|
|
||||||
|
Der Halm wird als Kette von Quads gebaut. Pro Ring:
|
||||||
|
t = 0 an der Wurzel .. 1 an der Spitze
|
||||||
|
UV.V = t -> genau die Windmaske
|
||||||
|
Breite = width * (1 - taper * t)
|
||||||
|
"""
|
||||||
|
seg = max(2, cfg["segments"])
|
||||||
|
h = cfg["height"] * (1.0 + rng.uniform(-1.0, 1.0) * cfg["height_var"] * 0.5)
|
||||||
|
w = cfg["width"] * (1.0 + rng.uniform(-0.25, 0.25))
|
||||||
|
|
||||||
|
# Halm-Ausrichtung: Richtung + Neigung aus der Mitte heraus
|
||||||
|
yaw = rng.uniform(0.0, math.tau)
|
||||||
|
lean = cfg["tilt"] * rng.uniform(0.35, 1.0)
|
||||||
|
root_r = cfg["spread"] * math.sqrt(rng.random())
|
||||||
|
rx, ry = math.cos(yaw) * root_r, math.sin(yaw) * root_r
|
||||||
|
|
||||||
|
# Variation je HALM (Kanal G) - eine Zahl fuer den ganzen Halm
|
||||||
|
g = rng.uniform(0.0, 1.0)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for i in range(seg + 1):
|
||||||
|
t = i / float(seg)
|
||||||
|
# Biegung: quadratisch, damit die Wurzel senkrecht bleibt
|
||||||
|
off = (cfg["bend"] * t * t) * h
|
||||||
|
# optionales Einrollen (Farn)
|
||||||
|
curl = cfg["curl"] * t * t * h
|
||||||
|
px = rx + math.cos(yaw) * (off * lean)
|
||||||
|
py = ry + math.sin(yaw) * (off * lean)
|
||||||
|
pz = h * t - curl
|
||||||
|
hw = 0.5 * w * max(0.02, 1.0 - cfg["taper"] * t)
|
||||||
|
# Breitenachse senkrecht zur Halm-Richtung
|
||||||
|
nx, ny = -math.sin(yaw), math.cos(yaw)
|
||||||
|
v_l = bm.verts.new((px - nx * hw, py - ny * hw, pz))
|
||||||
|
v_r = bm.verts.new((px + nx * hw, py + ny * hw, pz))
|
||||||
|
rows.append((v_l, v_r, t, g))
|
||||||
|
|
||||||
|
for i in range(seg):
|
||||||
|
a_l, a_r, t0, _ = rows[i]
|
||||||
|
b_l, b_r, t1, _ = rows[i + 1]
|
||||||
|
f = bm.faces.new((a_l, a_r, b_r, b_l))
|
||||||
|
for loop, (u, t) in zip(f.loops, ((0.0, t0), (1.0, t0), (1.0, t1), (0.0, t1))):
|
||||||
|
loop[uv_layer].uv = (u, t) # V = Wurzel 0 -> Spitze 1
|
||||||
|
ao = 0.35 + 0.65 * min(1.0, t * 1.6) # unten dunkler
|
||||||
|
loop[col_layer] = (t, g, ao, 1.0) # R=Wind, G=Halm, B=AO
|
||||||
|
|
||||||
|
|
||||||
|
def build_tuft(cfg, seed=0, name="Gras_01"):
|
||||||
|
rng = random.Random(seed)
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
bm = bmesh.new()
|
||||||
|
uv_layer = bm.loops.layers.uv.new("UVMap")
|
||||||
|
col_layer = bm.loops.layers.float_color.new("Cavity")
|
||||||
|
for _ in range(max(1, cfg["blades"])):
|
||||||
|
_add_blade(bm, uv_layer, col_layer, cfg, rng)
|
||||||
|
bm.to_mesh(me)
|
||||||
|
bm.free()
|
||||||
|
me.update()
|
||||||
|
return me
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Properties
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _preset_items(self, context):
|
||||||
|
return [(k, k.replace("_", " ").capitalize(), "") for k in PRESETS]
|
||||||
|
|
||||||
|
|
||||||
|
class GrassGenSettings(PropertyGroup):
|
||||||
|
preset: EnumProperty(name="Preset", items=_preset_items)
|
||||||
|
seed: IntProperty(name="Seed", default=0, min=0, max=9999)
|
||||||
|
|
||||||
|
art_name: StringProperty(
|
||||||
|
name="Art", default="",
|
||||||
|
description="Name fuer den Export. Leer = Preset-Name",
|
||||||
|
)
|
||||||
|
variant: IntProperty(name="Variante", default=1, min=1, max=99)
|
||||||
|
|
||||||
|
blades: IntProperty(name="Halme", default=14, min=1, max=80)
|
||||||
|
height: FloatProperty(name="Hoehe (m)", default=0.42, min=0.02, max=3.0)
|
||||||
|
height_var: FloatProperty(name="Hoehen-Streuung", default=0.45, min=0.0, max=1.0)
|
||||||
|
width: FloatProperty(name="Halmbreite (m)", default=0.030, min=0.002, max=0.3)
|
||||||
|
segments: IntProperty(
|
||||||
|
name="Segmente", default=4, min=2, max=10,
|
||||||
|
description="Unterteilungen je Halm. Mehr = weichere Biegung im Wind",
|
||||||
|
)
|
||||||
|
bend: FloatProperty(name="Biegung", default=0.42, min=0.0, max=2.0)
|
||||||
|
spread: FloatProperty(name="Streuung am Boden (m)", default=0.16, min=0.0, max=1.5)
|
||||||
|
tilt: FloatProperty(name="Neigung", default=0.55, min=0.0, max=2.0)
|
||||||
|
taper: FloatProperty(
|
||||||
|
name="Verjuengung", default=0.85, min=0.0, max=1.0,
|
||||||
|
description="1 = laeuft spitz zu, 0 = gleich breit bis oben",
|
||||||
|
)
|
||||||
|
curl: FloatProperty(name="Einrollen", default=0.0, min=0.0, max=1.0)
|
||||||
|
|
||||||
|
tri_budget: IntProperty(
|
||||||
|
name="Tri-Budget", default=400, min=0, max=20000,
|
||||||
|
description="Warnung bei Ueberschreitung (Gras/Blume laut ASSETS.md: 400)",
|
||||||
|
)
|
||||||
|
batch_count: IntProperty(name="Anzahl Varianten", default=1, min=1, max=32)
|
||||||
|
spacing: FloatProperty(name="Abstand", default=0.8, min=0.0, max=10.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg_from(settings):
|
||||||
|
return dict(
|
||||||
|
blades=settings.blades, height=settings.height, width=settings.width,
|
||||||
|
segments=settings.segments, bend=settings.bend, spread=settings.spread,
|
||||||
|
tilt=settings.tilt, taper=settings.taper, curl=settings.curl,
|
||||||
|
height_var=settings.height_var,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _art(settings):
|
||||||
|
return (settings.art_name.strip() or settings.preset).replace("_", " ").title().replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Operatoren
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class GRASSGEN_OT_apply_preset(Operator):
|
||||||
|
bl_idname = "grassgen.apply_preset"
|
||||||
|
bl_label = "Preset laden"
|
||||||
|
bl_description = "Uebernimmt die Werte des gewaehlten Presets in die Regler"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.grass_gen_settings
|
||||||
|
cfg = PRESETS.get(s.preset)
|
||||||
|
if not cfg:
|
||||||
|
self.report({'ERROR'}, "Preset unbekannt.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
for k, v in cfg.items():
|
||||||
|
if hasattr(s, k):
|
||||||
|
setattr(s, k, v)
|
||||||
|
self.report({'INFO'}, "Preset '%s' geladen." % s.preset)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class GRASSGEN_OT_randomize(Operator):
|
||||||
|
bl_idname = "grassgen.randomize"
|
||||||
|
bl_label = "Zufaellig"
|
||||||
|
bl_description = "Neuer Seed - gleiche Art, andere Halmverteilung"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.grass_gen_settings
|
||||||
|
s.seed = random.randint(0, 9999)
|
||||||
|
self.report({'INFO'}, "Seed %d" % s.seed)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class GRASSGEN_OT_create(Operator):
|
||||||
|
bl_idname = "object.grassgen_create"
|
||||||
|
bl_label = "Gras erzeugen"
|
||||||
|
bl_description = "Erzeugt Gras-Bueschel mit Wind-UV (V = Wurzel 0 -> Spitze 1)"
|
||||||
|
bl_options = {'REGISTER', 'UNDO'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
s = context.scene.grass_gen_settings
|
||||||
|
cfg = _cfg_from(s)
|
||||||
|
created, total = [], 0
|
||||||
|
try:
|
||||||
|
for i in range(s.batch_count):
|
||||||
|
name = "%s_%02d" % (_art(s), s.variant + i)
|
||||||
|
me = build_tuft(cfg, seed=s.seed + i * 733, name=name)
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
context.collection.objects.link(ob)
|
||||||
|
ob.location.x = i * s.spacing
|
||||||
|
created.append(ob)
|
||||||
|
total += sum(len(p.vertices) - 2 for p in me.polygons)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self.report({'ERROR'}, "Erzeugen fehlgeschlagen: %s" % exc)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
for o in context.view_layer.objects:
|
||||||
|
try:
|
||||||
|
o.select_set(False)
|
||||||
|
except (ReferenceError, RuntimeError):
|
||||||
|
pass
|
||||||
|
for ob in created:
|
||||||
|
ob.select_set(True)
|
||||||
|
if created:
|
||||||
|
context.view_layer.objects.active = created[0]
|
||||||
|
|
||||||
|
per = total // max(1, len(created))
|
||||||
|
msg = "%d Bueschel, %d Tris (je %d)" % (len(created), total, per)
|
||||||
|
if s.tri_budget and per > s.tri_budget:
|
||||||
|
self.report({'WARNING'}, msg + " - UEBER Budget (%d)" % s.tri_budget)
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, msg)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Panel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class VIEW3D_PT_grass_generator(Panel):
|
||||||
|
bl_label = "Grass Generator"
|
||||||
|
bl_idname = "VIEW3D_PT_grass_generator"
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = "Grass Gen"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
s = context.scene.grass_gen_settings
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Vorlage", icon='PRESET')
|
||||||
|
row = box.row(align=True)
|
||||||
|
row.prop(s, "preset", text="")
|
||||||
|
row.operator("grassgen.apply_preset", text="", icon='IMPORT')
|
||||||
|
row = box.row(align=True)
|
||||||
|
row.prop(s, "art_name", text="")
|
||||||
|
row.prop(s, "variant")
|
||||||
|
box.label(text="Name: %s_%02d" % (_art(s), s.variant), icon='INFO')
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Form")
|
||||||
|
box.prop(s, "blades")
|
||||||
|
box.prop(s, "height")
|
||||||
|
box.prop(s, "height_var")
|
||||||
|
box.prop(s, "width")
|
||||||
|
box.prop(s, "taper")
|
||||||
|
box.prop(s, "segments")
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Haltung")
|
||||||
|
box.prop(s, "bend")
|
||||||
|
box.prop(s, "tilt")
|
||||||
|
box.prop(s, "spread")
|
||||||
|
box.prop(s, "curl")
|
||||||
|
|
||||||
|
box = layout.box()
|
||||||
|
box.label(text="Zufall", icon='FILE_REFRESH')
|
||||||
|
row = box.row(align=True)
|
||||||
|
row.prop(s, "seed")
|
||||||
|
row.operator("grassgen.randomize", text="", icon='FILE_REFRESH')
|
||||||
|
box.prop(s, "batch_count")
|
||||||
|
box.prop(s, "spacing")
|
||||||
|
box.prop(s, "tri_budget")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
layout.operator("object.grassgen_create", icon='OUTLINER_OB_CURVES')
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text="UV0.V = Windmaske (Wurzel 0 - Spitze 1)", icon='INFO')
|
||||||
|
col.label(text="VCol: R=Wind G=Halm B=AO")
|
||||||
|
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
GrassGenSettings,
|
||||||
|
GRASSGEN_OT_apply_preset,
|
||||||
|
GRASSGEN_OT_randomize,
|
||||||
|
GRASSGEN_OT_create,
|
||||||
|
VIEW3D_PT_grass_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
bpy.types.Scene.grass_gen_settings = PointerProperty(type=GrassGenSettings)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
del bpy.types.Scene.grass_gen_settings
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import bpy, sys, importlib.util, os, tempfile
|
||||||
|
spec=importlib.util.spec_from_file_location("sgg", r"A:\eco\stylized_rock_generator\stylized_grass_generator.py")
|
||||||
|
g=importlib.util.module_from_spec(spec); sys.modules["sgg"]=g; spec.loader.exec_module(g)
|
||||||
|
try: g.unregister()
|
||||||
|
except Exception: pass
|
||||||
|
g.register()
|
||||||
|
fails=[]
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
s=bpy.context.scene.grass_gen_settings
|
||||||
|
s.preset='gras'; bpy.ops.grassgen.apply_preset()
|
||||||
|
s.art_name="Gras"; s.variant=1; s.batch_count=1
|
||||||
|
bpy.ops.object.grassgen_create()
|
||||||
|
ob=[o for o in bpy.context.scene.objects if o.type=='MESH'][0]
|
||||||
|
me=ob.data
|
||||||
|
tris=sum(len(p.vertices)-2 for p in me.polygons)
|
||||||
|
print("OBJEKT %s Tris=%d (Budget %d)" % (ob.name, tris, s.tri_budget))
|
||||||
|
if tris > s.tri_budget: fails.append("budget")
|
||||||
|
|
||||||
|
# --- Kernpruefung: UV.V muss mit der HOEHE korrelieren ---
|
||||||
|
uv=me.uv_layers[0].data
|
||||||
|
zs=[]; vs=[]
|
||||||
|
for poly in me.polygons:
|
||||||
|
for li in poly.loop_indices:
|
||||||
|
zs.append(me.vertices[me.loops[li].vertex_index].co.z)
|
||||||
|
vs.append(uv[li].uv[1])
|
||||||
|
zmin,zmax=min(zs),max(zs)
|
||||||
|
wurzel=[vs[i] for i in range(len(vs)) if zs[i] < zmin + (zmax-zmin)*0.05]
|
||||||
|
spitze=[vs[i] for i in range(len(vs)) if zs[i] > zmin + (zmax-zmin)*0.95]
|
||||||
|
print("UV.V an der WURZEL: %.3f an der SPITZE: %.3f (Bereich %.2f..%.2f)"
|
||||||
|
% (sum(wurzel)/len(wurzel), sum(spitze)/len(spitze), min(vs), max(vs)))
|
||||||
|
if sum(wurzel)/len(wurzel) > 0.12: fails.append("V_wurzel_nicht_0")
|
||||||
|
if sum(spitze)/len(spitze) < 0.80: fails.append("V_spitze_nicht_1")
|
||||||
|
if min(vs) < -0.001 or max(vs) > 1.001: fails.append("V_ausserhalb_0_1")
|
||||||
|
# U ueber die Breite
|
||||||
|
us=[uv[li].uv[0] for poly in me.polygons for li in poly.loop_indices]
|
||||||
|
print("UV.U Bereich: %.2f..%.2f" % (min(us),max(us)))
|
||||||
|
|
||||||
|
# --- Vertex-Farben als Zusatzebene ---
|
||||||
|
ca=me.color_attributes[0] if me.color_attributes else None
|
||||||
|
if ca is None: fails.append("keine_vcol")
|
||||||
|
else:
|
||||||
|
R=[d.color[0] for d in ca.data]; G=[d.color[1] for d in ca.data]; B=[d.color[2] for d in ca.data]
|
||||||
|
print("VCol R %.2f..%.2f | G %.2f..%.2f | B %.2f..%.2f" % (min(R),max(R),min(G),max(G),min(B),max(B)))
|
||||||
|
if max(G)-min(G) < 0.2: fails.append("G_keine_halmvariation")
|
||||||
|
# G muss PRO HALM konstant sein -> Anzahl unterschiedlicher Werte ~ Halmzahl
|
||||||
|
uniq=len({round(x,4) for x in G})
|
||||||
|
print("G unterschiedliche Werte: %d (Halme: %d)" % (uniq, s.blades))
|
||||||
|
if uniq > s.blades*1.5: fails.append("G_nicht_pro_halm")
|
||||||
|
|
||||||
|
# Ursprung unten
|
||||||
|
print("min_z = %.4f (soll ~0)" % zmin)
|
||||||
|
if abs(zmin) > 0.01: fails.append("ursprung")
|
||||||
|
|
||||||
|
# FBX-Roundtrip
|
||||||
|
tmp=tempfile.mkdtemp(); fp=os.path.join(tmp,"g.fbx")
|
||||||
|
ob.select_set(True); bpy.context.view_layer.objects.active=ob
|
||||||
|
bpy.ops.export_scene.fbx(filepath=fp, use_selection=True, colors_type='SRGB', use_triangles=True)
|
||||||
|
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||||
|
bpy.ops.import_scene.fbx(filepath=fp)
|
||||||
|
imp=[o for o in bpy.data.objects if o.type=='MESH'][0]
|
||||||
|
iuv=[d.uv[1] for d in imp.data.uv_layers[0].data]
|
||||||
|
ica=[a.name for a in imp.data.color_attributes]
|
||||||
|
print("NACH FBX: UV.V %.2f..%.2f VCol=%s" % (min(iuv),max(iuv), ica))
|
||||||
|
if max(iuv) < 0.9: fails.append("fbx_uv_verloren")
|
||||||
|
g.unregister()
|
||||||
|
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))
|
||||||
Reference in New Issue
Block a user