Neu: Stylized Asset Utils 1.0.0 (viertes Addon)

Fuer handgebaute Assets, die nicht durch einen Generator laufen (Pilze,
Baumstumpf, Erzfels, Props). Panel in fuenf einklappbaren Unter-Panels statt
einer langen Liste:

1 Vorbereiten  - Rotation/Scale anwenden, Ursprung unten-mittig, Normalen
2 Vertex-Farben - R=Cavity / G=Teil-oder-Moos-Maske / B=Hoehe / A=1
3 Benennung    - <Basis>_<NN>, Mesh-Daten ziehen mit
4 Pruefen      - Asset-Check auf alles, was in UE Aerger macht
5 Export       - FBX je Mesh mit colors_type='SRGB'

Zwei Bugs beim Bauen gefunden und gefixt:
- v.link_faces gibt es nur in bmesh, nicht auf Mesh.vertices -> lose Vertices
  ueber die tatsaechlich benutzten Indizes zaehlen.
- bpy.ops.paint.vertex_color_set braucht den Vertex-Paint-Modus und scheitert im
  Object-Mode mit "poll() failed". Zusammen mit vertex_color_dirt im selben try
  wurde dadurch der GANZE Bake stillschweigend uebersprungen (R/G/B blieben 1.0).
  Initialisierung laeuft jetzt direkt ueber Python.

Verifiziert am nachgebauten Pilz: Check meldet vorher Scale/Rotation/Ursprung,
nach "Vorbereiten" Scale 1/1/1, Rot 0/0/0, min_z 0.0000 und "ok". Teil-Maske
G=1 im Hut, 0 im Stiel. Nach FBX-Roundtrip G-Werte [0.0, 1.0] erhalten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:51:26 +02:00
co-authored by Claude Opus 4.8
parent 512d61e370
commit ef57050739
7 changed files with 707 additions and 1 deletions
+28
View File
@@ -7,6 +7,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` | > | **Stylized Grass Generator** | *Grass Gen* | Gras/Blumen/Farne mit Wind-UV fuer `SimpleGrassWind` |
> | **Stylized Asset Utils** | *Asset Utils* | **Handgebaute** Assets auf die Konvention bringen |
> >
> 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.
@@ -88,6 +89,33 @@ Abgestimmt auf `A:\eco\EcoGame` (UE 5.8). Kurzfassung:
--- ---
## Stylized Asset Utils
Fuer alles, was **von Hand** gebaut wird (Pilze, Baumstumpf, Erzfels, Props) und
deshalb nicht durch einen Generator laeuft. Panel in fuenf einklappbaren Schritten:
1. **Vorbereiten** — Rotation/Scale anwenden, Ursprung unten-mittig, optional
Normalen neu berechnen. Damit steht das Asset in UE bei Scale 1 auf dem Boden.
2. **Vertex-Farben** — dasselbe Layout wie beim Rock Generator
(`R` = Cavity, `G` = Maske, `B` = Hoehe, `A` = 1).
`G` wahlweise als **Teil-Maske**: bei Assets, die mit **Strg+J** aus mehreren
Objekten verbunden wurden (Pilz = Hut + Stiel), bekommt der oberste Teil 1 und
der Rest 0 — exakt statt geschaetzt. **Ein** Material toent damit beide Teile,
ein zweiter Material-Slot entfaellt. Einteilige Meshes bleiben unmarkiert.
3. **Benennung** — Auswahl nach `<Basis>_<NN>` umbenennen, Mesh-Daten ziehen mit.
4. **Pruefen** — Asset-Check: nicht angewendete Transforms, Ursprung nicht unten,
Tri-Budget, fehlende UV/Vertex-Farben, lose Vertices, Custom Split Normals.
Details in der System-Konsole.
5. **Export** — FBX je Mesh mit `colors_type='SRGB'`, Triangulate und waehlbarem
Smoothing.
**Falle, die dabei auffiel:** `bpy.ops.paint.vertex_color_set` braucht den
Vertex-Paint-Modus und scheitert im Object-Mode mit „poll() failed". Steht es mit
`vertex_color_dirt` im selben `try`, wird der ganze Bake stillschweigend
uebersprungen. Die Initialisierung laeuft deshalb direkt ueber Python.
---
## Stylized Grass Generator ## Stylized Grass Generator
Gras-, Blumen- und Farn-Bueschel. Der Kern ist die **Wind-UV**: Gras-, Blumen- und Farn-Bueschel. Der Kern ist die **Wind-UV**:
+22
View File
@@ -0,0 +1,22 @@
schema_version = "1.0.0"
id = "stylized_asset_utils"
version = "1.0.0"
name = "Stylized Asset Utils"
tagline = "Handgebaute Assets auf EcoGame-Konvention bringen"
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",
]
+2 -1
View File
@@ -27,7 +27,8 @@ New-Item -ItemType Directory -Force -Path $dist | Out-Null
$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" } @{ name = "grass"; manifest = "blender_manifest_grass.toml"; source = "stylized_grass_generator.py" },
@{ name = "utils"; manifest = "blender_manifest_utils.toml"; source = "stylized_asset_utils.py" }
) )
# Alte Zips weg, damit geloeschte Versionen nicht im Listing haengen bleiben. # Alte Zips weg, damit geloeschte Versionen nicht im Listing haengen bleiben.
+25
View File
@@ -2,6 +2,31 @@
"version": "v1", "version": "v1",
"blocklist": [], "blocklist": [],
"data": [ "data": [
{
"schema_version": "1.0.0",
"id": "stylized_asset_utils",
"name": "Stylized Asset Utils",
"tagline": "Handgebaute Assets auf EcoGame-Konvention bringen",
"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_asset_utils-1.0.0.zip",
"archive_size": 6340,
"archive_hash": "sha256:af8f9e95054432e4e7eea09b520b30a659ab354795191dcab0e5504c518306d3"
},
{ {
"schema_version": "1.0.0", "schema_version": "1.0.0",
"id": "stylized_grass_generator", "id": "stylized_grass_generator",
Binary file not shown.
+545
View File
@@ -0,0 +1,545 @@
bl_info = {
"name": "Stylized Asset Utils",
"author": "D4rkst3r",
"version": (1, 0, 0),
"blender": (4, 2, 0),
"location": "View3D > Sidebar > Asset Utils",
"description": (
"Werkzeuge fuer HANDGEBAUTE Assets: Transform/Ursprung, Vertex-Farben, "
"Benennung, Asset-Check und FBX-Export nach EcoGame-Konvention."
),
"category": "Object",
}
# ---------------------------------------------------------------------------
# Wofuer dieses Addon da ist
#
# Rock-, Tree- und Grass-Generator erzeugen Assets nach Konvention. Was von HAND
# gebaut wird (Pilze, Baumstumpf, Erzfels, Props) laeuft daran vorbei. Dieses
# Addon bringt beliebige Meshes auf denselben Stand:
# - Transform anwenden + Ursprung unten-mittig
# - Vertex-Farben R=Cavity / G=Teil-oder-Moos / B=Hoehe / A=1
# - Benennung nach Schema
# - Asset-Check: was wuerde in UE Aerger machen?
# - FBX-Export mit den richtigen Schaltern (Vertex-Farben!)
# ---------------------------------------------------------------------------
import bpy
import os
import math
import random
from bpy.props import (
IntProperty, FloatProperty, EnumProperty, BoolProperty,
StringProperty, PointerProperty,
)
from bpy.types import Operator, Panel, PropertyGroup
# ---------------------------------------------------------------------------
# Helfer
# ---------------------------------------------------------------------------
def _deselect_all(context):
for o in list(context.view_layer.objects):
try:
o.select_set(False)
except (ReferenceError, AttributeError, RuntimeError):
pass
def _ensure_object_mode(context):
if context.mode != 'OBJECT':
try:
bpy.ops.object.mode_set(mode='OBJECT')
except RuntimeError:
pass
def _activate(context, ob):
_deselect_all(context)
context.view_layer.objects.active = ob
ob.select_set(True)
def _tris(me):
return sum(len(p.vertices) - 2 for p in me.polygons)
def _mesh_shells(me):
"""Zusammenhaengende Teile als Mengen von Polygon-Indizes.
Assets, die aus mehreren Objekten mit Strg+J verbunden wurden (Pilz =
Hut + Stiel), bleiben geometrisch getrennt - so laesst sich die Teil-Maske
EXAKT bilden statt sie ueber Normalen zu schaetzen.
"""
adj = {}
for poly in me.polygons:
for ek in poly.edge_keys:
adj.setdefault(ek, []).append(poly.index)
seen, shells = set(), []
for poly in me.polygons:
if poly.index in seen:
continue
stack, comp = [poly.index], set()
while stack:
pi = stack.pop()
if pi in comp:
continue
comp.add(pi)
for ek in me.polygons[pi].edge_keys:
for nb in adj.get(ek, ()):
if nb not in comp:
stack.append(nb)
seen |= comp
shells.append(comp)
return shells
# ---------------------------------------------------------------------------
# Operatoren: Vorbereiten
# ---------------------------------------------------------------------------
class ASSETUTILS_OT_prepare(Operator):
bl_idname = "assetutils.prepare"
bl_label = "Transform anwenden + Ursprung unten"
bl_description = ("Wendet Rotation und Skalierung an und setzt den Ursprung "
"unten-mittig - so steht das Asset in UE bei Scale 1 auf dem Boden")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.asset_utils_settings
targets = [o for o in context.selected_objects if o.type == 'MESH']
if not targets:
self.report({'ERROR'}, "Kein Mesh ausgewaehlt.")
return {'CANCELLED'}
_ensure_object_mode(context)
scene = context.scene
prev_cursor = scene.cursor.location.copy()
for ob in targets:
_activate(context, ob)
if s.apply_transforms:
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
if s.origin_bottom:
mw = ob.matrix_world
co = [mw @ v.co for v in ob.data.vertices]
if co:
cx = sum(c.x for c in co) / len(co)
cy = sum(c.y for c in co) / len(co)
zmin = min(c.z for c in co)
scene.cursor.location = (cx, cy, zmin)
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
if s.recalc_normals:
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT')
scene.cursor.location = prev_cursor
self.report({'INFO'}, "%d Asset(s) vorbereitet." % len(targets))
return {'FINISHED'}
# ---------------------------------------------------------------------------
# Operatoren: Vertex-Farben
# ---------------------------------------------------------------------------
class ASSETUTILS_OT_bake_vcol(Operator):
bl_idname = "assetutils.bake_vcol"
bl_label = "Vertex-Farben backen"
bl_description = ("Backt R=Cavity, G=Teil-/Moos-Maske, B=Hoehe, A=1 in das "
"Farbattribut 'Cavity'")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.asset_utils_settings
targets = [o for o in context.selected_objects if o.type == 'MESH']
if not targets:
self.report({'ERROR'}, "Kein Mesh ausgewaehlt.")
return {'CANCELLED'}
_ensure_object_mode(context)
notes = []
for ob in targets:
me = ob.data
if not me.polygons:
continue
layer = me.color_attributes.get("Cavity")
if layer is None:
layer = me.color_attributes.new(name="Cavity", type='BYTE_COLOR',
domain='CORNER')
me.color_attributes.active_color = layer
idx = me.color_attributes.find("Cavity")
if idx >= 0:
me.color_attributes.render_color_index = idx
_activate(context, ob)
# FALLE: bpy.ops.paint.vertex_color_set braucht den VERTEX-PAINT-Modus
# und schlaegt im Object-Mode mit "poll() failed" fehl. Steht es mit
# vertex_color_dirt im selben try-Block, wird der ganze Bake
# uebersprungen. Deshalb hier direkt in Python auf Weiss setzen -
# das geht immer und ist schneller.
for i in range(len(layer.data)):
layer.data[i].color = (1.0, 1.0, 1.0, 1.0)
try:
bpy.ops.paint.vertex_color_dirt(
blur_strength=1.0, blur_iterations=s.cavity_blur,
clean_angle=math.pi, dirt_angle=0.0,
dirt_only=False, normalize=True,
)
except RuntimeError as exc:
self.report({'WARNING'}, "%s: Cavity uebersprungen (%s)" % (ob.name, exc))
data = layer.data
cav_raw = [data[i].color[0] for i in range(len(data))]
zs = [v.co.z for v in me.vertices]
zmin, zmax = min(zs), max(zs)
span = max(zmax - zmin, 1e-6)
part = {}
if s.g_mode == 'SHELLS':
shells = _mesh_shells(me)
if len(shells) >= 2:
infos = []
for sh in shells:
vs = {vi for pi in sh for vi in me.polygons[pi].vertices}
zt = max(me.vertices[v].co.z for v in vs)
xs = [me.vertices[v].co.x for v in vs]
ys = [me.vertices[v].co.y for v in vs]
infos.append((sh, zt, max(max(xs) - min(xs), max(ys) - min(ys))))
top = max(infos, key=lambda t: (t[1], t[2]))
for sh, _, _ in infos:
for pi in sh:
part[pi] = 1.0 if sh is top[0] else 0.0
notes.append("%s: %d Teile" % (ob.name, len(shells)))
else:
# Nur ein Teil -> nichts markieren, statt etwas zu behaupten
part = {p.index: 0.0 for p in me.polygons}
notes.append("%s: 1 Teil (G=0)" % ob.name)
rng = random.Random(s.moss_seed)
vnoise = [rng.random() for _ in range(len(me.vertices))]
for poly in me.polygons:
nz = poly.normal.z
for li in poly.loop_indices:
vi = me.loops[li].vertex_index
c_raw = cav_raw[li]
r = max(0.0, min(1.0, 1.0 - (1.0 - c_raw) * s.cavity_strength))
b = max(0.0, min(1.0, (me.vertices[vi].co.z - zmin) / span))
if s.g_mode == 'SHELLS':
g = part.get(poly.index, 0.0)
else:
up = max(0.0, nz)
crev = 1.0 - c_raw
n = 1.0 - s.moss_noise + s.moss_noise * vnoise[vi]
g = max(0.0, min(1.0, (up * 0.7 + crev * 0.5) * n * s.moss_amount * 1.6))
data[li].color = (r, g, b, 1.0)
msg = "Vertex-Farben auf %d Mesh(es)." % len(targets)
if notes:
msg += " " + ", ".join(notes[:4])
self.report({'INFO'}, msg)
return {'FINISHED'}
# ---------------------------------------------------------------------------
# Operatoren: Benennung
# ---------------------------------------------------------------------------
class ASSETUTILS_OT_rename(Operator):
bl_idname = "assetutils.rename"
bl_label = "Umbenennen"
bl_description = "Benennt die Auswahl nach <Basis>_<NN> und zieht die Mesh-Daten mit"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
s = context.scene.asset_utils_settings
base = s.rename_base.strip()
if not base:
self.report({'ERROR'}, "Bitte einen Basisnamen eingeben.")
return {'CANCELLED'}
targets = sorted([o for o in context.selected_objects if o.type == 'MESH'],
key=lambda o: o.name)
if not targets:
self.report({'ERROR'}, "Kein Mesh ausgewaehlt.")
return {'CANCELLED'}
for i, ob in enumerate(targets):
name = base if (len(targets) == 1 and not s.rename_numbered) \
else "%s_%02d" % (base, s.rename_start + i)
ob.name = name
ob.data.name = name
self.report({'INFO'}, "%d Objekt(e) umbenannt." % len(targets))
return {'FINISHED'}
# ---------------------------------------------------------------------------
# Operatoren: Asset-Check
# ---------------------------------------------------------------------------
class ASSETUTILS_OT_check(Operator):
bl_idname = "assetutils.check"
bl_label = "Asset-Check"
bl_description = ("Prueft die Auswahl auf die Dinge, die in UE Aerger machen: "
"Transform, Ursprung, Tri-Budget, UV, Vertex-Farben, Normalen")
bl_options = {'REGISTER'}
def execute(self, context):
s = context.scene.asset_utils_settings
targets = [o for o in context.selected_objects if o.type == 'MESH']
if not targets:
self.report({'ERROR'}, "Kein Mesh ausgewaehlt.")
return {'CANCELLED'}
problems = 0
print("\n=== Asset-Check ===")
for ob in targets:
me = ob.data
issues = []
if any(abs(v - 1.0) > 1e-4 for v in ob.scale):
issues.append("Scale nicht angewendet %s"
% [round(v, 3) for v in ob.scale])
if any(abs(v) > 1e-4 for v in ob.rotation_euler):
issues.append("Rotation nicht angewendet")
zs = [(ob.matrix_world @ v.co).z - ob.location.z for v in me.vertices]
if zs and abs(min(zs)) > s.origin_tol:
issues.append("Ursprung nicht unten (min_z %.3f)" % min(zs))
t = _tris(me)
if s.tri_budget and t > s.tri_budget:
issues.append("%d Tris > Budget %d" % (t, s.tri_budget))
if not me.uv_layers:
issues.append("kein UV")
if not me.color_attributes:
issues.append("keine Vertex-Farben")
# link_faces gibt es nur in bmesh - auf einem Mesh ueber die
# tatsaechlich benutzten Vertex-Indizes gehen.
used = {vi for poly in me.polygons for vi in poly.vertices}
n_loose = len(me.vertices) - len(used)
if n_loose:
issues.append("%d lose Vertices" % n_loose)
if me.has_custom_normals:
issues.append("Custom Split Normals (UE ggf. 'Import Normals' noetig)")
shells = len(_mesh_shells(me))
info = "%-24s %5d Tris %d Teil(e)" % (ob.name, t, shells)
if issues:
problems += 1
print(" %s\n -> %s" % (info, "; ".join(issues)))
else:
print(" %s ok" % info)
print("=== %d von %d Asset(s) mit Hinweisen ===\n" % (problems, len(targets)))
if problems:
self.report({'WARNING'},
"%d von %d Asset(s) mit Hinweisen - Details in der System-Konsole."
% (problems, len(targets)))
else:
self.report({'INFO'}, "Alle %d Asset(s) in Ordnung." % len(targets))
return {'FINISHED'}
# ---------------------------------------------------------------------------
# Operatoren: Export
# ---------------------------------------------------------------------------
class ASSETUTILS_OT_export(Operator):
bl_idname = "assetutils.export"
bl_label = "FBX exportieren"
bl_description = "Exportiert jedes ausgewaehlte Mesh als eigene FBX (EcoGame-Settings)"
bl_options = {'REGISTER'}
def execute(self, context):
s = context.scene.asset_utils_settings
folder = bpy.path.abspath(s.export_dir) if s.export_dir else ""
if not folder or not os.path.isdir(folder):
self.report({'ERROR'}, "Bitte einen gueltigen Export-Ordner waehlen.")
return {'CANCELLED'}
targets = [o for o in context.selected_objects if o.type == 'MESH']
if not targets:
self.report({'ERROR'}, "Kein Mesh ausgewaehlt.")
return {'CANCELLED'}
_ensure_object_mode(context)
done = 0
for ob in targets:
_activate(context, ob)
fp = os.path.join(folder, ob.name + ".fbx")
try:
bpy.ops.export_scene.fbx(
filepath=fp, use_selection=True, object_types={'MESH'},
use_mesh_modifiers=True,
mesh_smooth_type='FACE' if s.smooth_type == 'FACE' else 'EDGE',
use_triangles=True,
colors_type='SRGB', # Vertex-Farben mitnehmen
add_leaf_bones=False, bake_anim=False, path_mode='AUTO',
)
done += 1
except RuntimeError as exc:
self.report({'WARNING'}, "%s: %s" % (ob.name, exc))
self.report({'INFO'}, "%d FBX nach %s" % (done, folder))
return {'FINISHED'}
# ---------------------------------------------------------------------------
# Properties
# ---------------------------------------------------------------------------
class AssetUtilsSettings(PropertyGroup):
apply_transforms: BoolProperty(name="Rotation + Scale anwenden", default=True)
origin_bottom: BoolProperty(name="Ursprung unten-mittig", default=True)
recalc_normals: BoolProperty(name="Normalen neu berechnen", default=False)
g_mode: EnumProperty(
name="G-Kanal",
items=[
('SHELLS', "Teil-Maske", "Oberster getrennter Teil = 1 (z.B. Pilzhut)"),
('MOSS', "Moos/Schmutz", "Oberseite + Ritzen + Rauschen"),
],
default='SHELLS',
)
cavity_strength: FloatProperty(name="Cavity-Staerke", default=0.7, min=0.0, max=1.0)
cavity_blur: IntProperty(name="Weichzeichnen", default=1, min=0, max=5)
moss_amount: FloatProperty(name="Moos-Menge", default=0.55, min=0.0, max=1.0)
moss_noise: FloatProperty(name="Moos-Aufbruch", default=0.55, min=0.0, max=1.0)
moss_seed: IntProperty(name="Moos-Seed", default=0, min=0, max=9999)
rename_base: StringProperty(name="Basisname", default="")
rename_start: IntProperty(name="Startnummer", default=1, min=0, max=99)
rename_numbered: BoolProperty(name="Immer nummerieren", default=False)
tri_budget: IntProperty(
name="Tri-Budget", default=2500, min=0, max=100000,
description="0 = nicht pruefen. ASSETS.md: Gras 400, Busch 800, Baum 2500",
)
origin_tol: FloatProperty(name="Ursprung-Toleranz (m)", default=0.01,
min=0.0, max=0.5)
export_dir: StringProperty(name="Export-Ordner", default="", subtype='DIR_PATH')
smooth_type: EnumProperty(
name="Smoothing",
items=[('FACE', "Faceted", "Harte Facetten"),
('EDGE', "Edge", "Weiche Kanten mit Sharp-Edges")],
default='FACE',
)
# ---------------------------------------------------------------------------
# Panels (Haupt-Panel + einklappbare Unter-Panels)
# ---------------------------------------------------------------------------
class VIEW3D_PT_asset_utils(Panel):
bl_label = "Asset Utils"
bl_idname = "VIEW3D_PT_asset_utils"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Asset Utils"
def draw(self, context):
n = len([o for o in context.selected_objects if o.type == 'MESH'])
self.layout.label(text="%d Mesh(es) ausgewaehlt" % n,
icon='OUTLINER_OB_MESH' if n else 'INFO')
class _Sub(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Asset Utils"
bl_parent_id = "VIEW3D_PT_asset_utils"
bl_options = {'DEFAULT_CLOSED'}
class VIEW3D_PT_au_prepare(_Sub):
bl_label = "1 - Vorbereiten"
bl_options = set() # offen lassen
def draw(self, context):
s = context.scene.asset_utils_settings
col = self.layout.column(align=True)
col.prop(s, "apply_transforms")
col.prop(s, "origin_bottom")
col.prop(s, "recalc_normals")
self.layout.operator("assetutils.prepare", icon='OBJECT_ORIGIN')
class VIEW3D_PT_au_vcol(_Sub):
bl_label = "2 - Vertex-Farben"
bl_options = set()
def draw(self, context):
s = context.scene.asset_utils_settings
lay = self.layout
lay.prop(s, "g_mode", expand=True)
col = lay.column(align=True)
col.prop(s, "cavity_strength")
col.prop(s, "cavity_blur")
if s.g_mode == 'MOSS':
col.separator()
col.prop(s, "moss_amount")
col.prop(s, "moss_noise")
col.prop(s, "moss_seed")
lay.label(text="R=Cavity G=%s B=Hoehe"
% ("Teil" if s.g_mode == 'SHELLS' else "Moos"), icon='INFO')
lay.operator("assetutils.bake_vcol", icon='BRUSH_DATA')
class VIEW3D_PT_au_name(_Sub):
bl_label = "3 - Benennung"
def draw(self, context):
s = context.scene.asset_utils_settings
col = self.layout.column(align=True)
col.prop(s, "rename_base")
row = col.row(align=True)
row.prop(s, "rename_start")
row.prop(s, "rename_numbered", text="", icon='LINENUMBERS_ON')
self.layout.operator("assetutils.rename", icon='SORTALPHA')
class VIEW3D_PT_au_check(_Sub):
bl_label = "4 - Pruefen"
bl_options = set()
def draw(self, context):
s = context.scene.asset_utils_settings
col = self.layout.column(align=True)
col.prop(s, "tri_budget")
col.prop(s, "origin_tol")
self.layout.operator("assetutils.check", icon='CHECKMARK')
class VIEW3D_PT_au_export(_Sub):
bl_label = "5 - Export"
def draw(self, context):
s = context.scene.asset_utils_settings
col = self.layout.column(align=True)
col.prop(s, "export_dir")
col.prop(s, "smooth_type", expand=True)
self.layout.operator("assetutils.export", icon='EXPORT')
self.layout.label(text="UE: Vertex Color = Replace", icon='INFO')
classes = (
AssetUtilsSettings,
ASSETUTILS_OT_prepare,
ASSETUTILS_OT_bake_vcol,
ASSETUTILS_OT_rename,
ASSETUTILS_OT_check,
ASSETUTILS_OT_export,
VIEW3D_PT_asset_utils,
VIEW3D_PT_au_prepare,
VIEW3D_PT_au_vcol,
VIEW3D_PT_au_name,
VIEW3D_PT_au_check,
VIEW3D_PT_au_export,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.asset_utils_settings = PointerProperty(type=AssetUtilsSettings)
def unregister():
del bpy.types.Scene.asset_utils_settings
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()
+85
View File
@@ -0,0 +1,85 @@
import bpy, sys, importlib.util, os, tempfile, math
spec=importlib.util.spec_from_file_location("sau", r"A:\eco\stylized_rock_generator\stylized_asset_utils.py")
u=importlib.util.module_from_spec(spec); sys.modules["sau"]=u; spec.loader.exec_module(u)
try: u.unregister()
except Exception: pass
u.register()
fails=[]
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
# Pilz wie beim User: Hut + Stiel getrennt, dann Join. Mit ABSICHT unangewendeter
# Transform und Ursprung nicht unten - das soll der Check finden.
bpy.ops.mesh.primitive_cylinder_add(vertices=10, radius=0.028, depth=0.126, location=(0,0,0.5))
st=bpy.context.active_object
bpy.ops.mesh.primitive_uv_sphere_add(segments=14, ring_count=7, radius=0.042, location=(0,0,0.567))
hu=bpy.context.active_object; hu.scale=(1,1,0.5); bpy.ops.object.transform_apply(scale=True)
for o in bpy.context.selected_objects: o.select_set(False)
st.select_set(True); hu.select_set(True); bpy.context.view_layer.objects.active=st
bpy.ops.object.join()
p=bpy.context.active_object; p.name="Pilz_roh"
p.rotation_euler=(0.16,0.11,-2.26); p.scale=(0.6,0.6,0.6)
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.05, location=(1,0,0)) # UV vorhanden
p.select_set(True); bpy.context.view_layer.objects.active=p
for o in bpy.context.selected_objects:
if o is not p: o.select_set(False)
s=bpy.context.scene.asset_utils_settings
s.tri_budget=400; s.g_mode='SHELLS'
print("--- Check VOR dem Vorbereiten (soll meckern) ---")
bpy.ops.assetutils.check()
print("Scale vorher: %s Rot: %s" % ([round(v,2) for v in p.scale],
[round(math.degrees(v),1) for v in p.rotation_euler]))
bpy.ops.assetutils.prepare()
zs=[(p.matrix_world @ v.co).z - p.location.z for v in p.data.vertices]
print("NACH prepare: Scale %s Rot %s min_z %.4f"
% ([round(v,3) for v in p.scale],
[round(math.degrees(v),1) for v in p.rotation_euler], min(zs)))
if any(abs(v-1)>1e-4 for v in p.scale): fails.append("scale_nicht_angewendet")
if any(abs(v)>1e-4 for v in p.rotation_euler): fails.append("rot_nicht_angewendet")
if abs(min(zs))>0.01: fails.append("ursprung_nicht_unten")
bpy.ops.assetutils.bake_vcol()
ca=p.data.color_attributes.get("Cavity")
if ca is None: fails.append("keine_vcol")
else:
shells=u._mesh_shells(p.data)
infos=[]
for sh in shells:
vs={vi for pi in sh for vi in p.data.polygons[pi].vertices}
infos.append((sh, max(p.data.vertices[v].co.z for v in vs)))
infos.sort(key=lambda t:-t[1])
g_top={round(ca.data[li].color[1],3) for pi in infos[0][0] for li in p.data.polygons[pi].loop_indices}
g_rest={round(ca.data[li].color[1],3) for sh,_ in infos[1:] for pi in sh for li in p.data.polygons[pi].loop_indices}
R=[d.color[0] for d in ca.data]; B=[d.color[2] for d in ca.data]; A=[d.color[3] for d in ca.data]
print("VCol: G_Hut=%s G_Stiel=%s | R %.2f..%.2f | B %.2f..%.2f | A %.2f"
% (sorted(g_top), sorted(g_rest), min(R),max(R),min(B),max(B),min(A)))
if g_top!={1.0}: fails.append("G_hut")
if g_rest!={0.0}: fails.append("G_stiel")
if max(B)-min(B)<0.8: fails.append("B_flach")
if min(A)<0.99: fails.append("A")
s.rename_base="Pilz_01"; s.rename_numbered=False
bpy.ops.assetutils.rename()
print("Umbenannt zu: %s (Mesh: %s)" % (p.name, p.data.name))
if p.name!="Pilz_01": fails.append("rename")
print("--- Check NACH dem Vorbereiten (soll sauber sein) ---")
bpy.ops.assetutils.check()
out=tempfile.mkdtemp(); s.export_dir=out
bpy.ops.assetutils.export()
fbx=[f for f in os.listdir(out) if f.endswith(".fbx")]
print("Export:", fbx)
if not fbx: fails.append("export")
else:
for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True)
bpy.ops.import_scene.fbx(filepath=os.path.join(out,fbx[0]))
imp=[o for o in bpy.data.objects if o.type=='MESH'][0]
ic=[a.name for a in imp.data.color_attributes]
ig={round(d.color[1],2) for d in imp.data.color_attributes[0].data} if ic else set()
print("Nach FBX: VCol=%s G-Werte=%s" % (ic, sorted(ig)))
if not ic: fails.append("fbx_vcol_verloren")
u.unregister()
print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))