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:
@@ -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**:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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.
|
||||||
|
|||||||
Vendored
+25
@@ -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",
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -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()
|
||||||
@@ -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))
|
||||||
Reference in New Issue
Block a user