U2A Blender/FBX Pipeline

Reproduzierbarer Export aus den Blender-Quellen der Godot-Referenz nach FBX,
geprüft auf beiden Seiten; sechs repräsentative Assets vollständig in Unity
verifiziert (normale Einheit g01, Einheit mit Skelett Funkenlanze, Turm
Grenzwacht, Ancient, Schlundkrone, Vegetation/Kartenrequisiten), dazu Bastion,
Kernwacht und Bossgrotte für die Szene.

- tools/blender: Manifest mit Exportregeln je Asset, Exportskript mit Prüfungen
  (Einheiten, Maßstab, Achsen, Vorderseite, Mesh-Namen, Dreiecke, Materialslots,
  Normalen, Tangenten, Vertexfarben, Maße, Root Bone, Bone-Namen, Clips, Blend
  Shapes) gegen die Manifeste der Godot-GLBs, FBX-Rundreise, byteidentische
  Ausgabe (feste Uhrzeit, PYTHONHASHSEED).
- Regeln wie beim GLB-Export: HIGH-RES-Modifikatoren weglassen, logische Namen,
  Skin-Teile zusammenfügen; 180°-Drehung, damit die Vorderseite nach Unitys
  Achsen-Bake auf +Z liegt (Unity = (-x, y, z) der Godot-Koordinaten).
- Unity: Importregeln (Postprocessor), Verifier gegen das Blender-Protokoll,
  Vorschaubilder; URP-Asset, Linear-Farbraum, URP-Settings nach
  Assets/Luneraria/Settings/Rendering.
- Ergebnis: 176/176 Blender-Prüfungen, 128/128 Unity-Prüfungen, Dreiecke gleich
  den Godot-GLBs, Maße auf 0,05 mm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin Stickelbroeck 2026-09-30 15:41:24 +02:00
parent 9bf397e765
commit 49cb4af407
87 changed files with 13976 additions and 41 deletions

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 50ac3333ebb2c4d7a9f94a41495626b4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1f40c6a05032444fdbf7dd895f45db6f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4a082f612d76242bd8aebf43e93aeca2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: a0437d8104fe34013aacb4fb7c2a5a6c
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,602 @@
{
"id": "bastion",
"group": "Buildings",
"source": "assets-source/luneraria/g2/bastion.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['Crystal', 'Debris', 'Lower', 'Pennant', 'Ring', 'Ruin', 'Shard_0', 'Shard_1', 'Shard_2', 'Upper', 'Upper_Damaged']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "9364 fbx / 9364 godot"
},
{
"check": "mesh_names_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "materials_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_match_godot",
"ok": true,
"detail": "largest difference 0.00000 m"
},
{
"check": "no_armature",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "9364 / 9364"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
}
],
"source_sha256": "5ac677fe95f3c65364fa4003b9cae694ad58734b157238f196f4d926de655d60",
"removed_modifiers": 0,
"kind": "static",
"fps": 30.0,
"objects": 11,
"meshes": {
"Lower": {
"mesh": "Lower",
"triangles": 658,
"vertices": 368,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Metal_Iron"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.7395,
0.0,
-2.25
],
"max": [
1.7395,
2.6655,
1.6437
]
}
},
"Upper": {
"mesh": "Upper",
"triangles": 3272,
"vertices": 1722,
"ngons": 0,
"materials": [
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Rune_Team",
"LN_Stone_Dark",
"LN_Metal_Iron",
"LN_Wood"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.5347,
2.4409,
-1.5347
],
"max": [
1.5347,
7.1605,
1.5347
]
}
},
"Upper_Damaged": {
"mesh": "Upper_Damaged",
"triangles": 2948,
"vertices": 1554,
"ngons": 0,
"materials": [
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Rune_Team",
"LN_Stone_Dark",
"LN_Metal_Iron",
"LN_Wood"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.5368,
2.3209,
-1.5372
],
"max": [
1.4687,
7.1605,
1.5405
]
}
},
"Debris": {
"mesh": "Debris",
"triangles": 316,
"vertices": 178,
"ngons": 0,
"materials": [
"LN_Stone_Pale",
"LN_Metal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-2.1156,
-0.3891,
-1.7064
],
"max": [
1.8813,
0.54,
1.9496
]
}
},
"Crystal": {
"mesh": "Crystal",
"triangles": 24,
"vertices": 14,
"ngons": 0,
"materials": [
"LN_Crystal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.4934,
5.54,
-0.4411
],
"max": [
0.4885,
7.06,
0.409
]
}
},
"Ring": {
"mesh": "Ring",
"triangles": 640,
"vertices": 320,
"ngons": 0,
"materials": [
"LN_Metal_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.865,
6.15,
-0.865
],
"max": [
0.865,
6.25,
0.865
]
}
},
"Shard_0": {
"mesh": "Shard_0",
"triangles": 20,
"vertices": 12,
"ngons": 0,
"materials": [
"LN_Crystal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.1308,
6.17,
-0.106
],
"max": [
-0.9277,
6.61,
0.1012
]
}
},
"Shard_1": {
"mesh": "Shard_1",
"triangles": 20,
"vertices": 12,
"ngons": 0,
"materials": [
"LN_Crystal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
0.397,
6.17,
0.7782
],
"max": [
0.601,
6.61,
0.9923
]
}
},
"Shard_2": {
"mesh": "Shard_2",
"triangles": 20,
"vertices": 12,
"ngons": 0,
"materials": [
"LN_Crystal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
0.3992,
6.17,
-0.9903
],
"max": [
0.6009,
6.61,
-0.779
]
}
},
"Pennant": {
"mesh": "Pennant",
"triangles": 64,
"vertices": 45,
"ngons": 0,
"materials": [
"LN_Cloth_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
0.06,
5.65,
-1.46
],
"max": [
0.76,
6.8,
-1.46
]
}
},
"Ruin": {
"mesh": "Ruin",
"triangles": 1382,
"vertices": 770,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Crystal_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-2.0369,
-0.3691,
-2.5389
],
"max": [
2.9008,
2.6896,
1.9476
]
}
}
},
"armatures": {},
"empties": {},
"bounds_unity": {
"min": [
-2.1156,
-0.3891,
-2.5389
],
"max": [
2.9008,
7.1605,
1.9496
]
},
"clips": [],
"expected_from_godot": {
"triangles": 9364,
"bounds_godot": {
"min": [
-1.7395,
0.0,
-2.25
],
"max": [
1.7395,
7.1605,
1.6437
]
},
"resource_path": "res://assets/luneraria/g2/bastion.glb"
},
"meshes_without_uv": [
"Crystal",
"Debris",
"Lower",
"Shard_0",
"Shard_1",
"Shard_2"
],
"triangles": 9364,
"fbx": "Assets/Luneraria/Art/Buildings/bastion.fbx",
"fbx_bytes": 618812,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b6d3f51223b7f4f00b4ae9778a64127e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: 6633f0fd87c194e13a26c46c8a57a3a8
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,632 @@
{
"id": "bossgrotte",
"group": "Buildings",
"source": "assets-source/luneraria/g2/bossgrotte.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['Altar', 'Cave', 'CaveGlow', 'Channel', 'Floor', 'Gate', 'PillarCrystals', 'Pillars', 'Platform', 'Rocks', 'Runes', 'Source']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "15182 fbx / 15182 godot"
},
{
"check": "mesh_names_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "materials_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_match_godot",
"ok": true,
"detail": "largest difference 0.00000 m"
},
{
"check": "no_armature",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "15182 / 15182"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
}
],
"source_sha256": "c3a87a8e7b0b8e0778ba7f9f274584e98e5c70a8229bc1cbee7ed9fd6aba7d9e",
"removed_modifiers": 0,
"kind": "static",
"fps": 30.0,
"objects": 12,
"meshes": {
"Floor": {
"mesh": "Floor",
"triangles": 2108,
"vertices": 1097,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Grotto"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-7.7775,
-0.4,
-7.7874
],
"max": [
7.7755,
0.26,
7.7976
]
}
},
"Runes": {
"mesh": "Runes",
"triangles": 1164,
"vertices": 712,
"ngons": 0,
"materials": [
"LN_Rune_Grotto"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-4.72,
0.104,
-7.9255
],
"max": [
7.9255,
5.899,
4.72
]
}
},
"Platform": {
"mesh": "Platform",
"triangles": 808,
"vertices": 433,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Grotto"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.8,
-0.02,
-3.8
],
"max": [
3.8,
0.125,
0.8
]
}
},
"Altar": {
"mesh": "Altar",
"triangles": 552,
"vertices": 280,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Grotto"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
1.7789,
-0.05,
-4.4738
],
"max": [
4.4738,
3.45,
-1.7789
]
}
},
"Source": {
"mesh": "Source",
"triangles": 192,
"vertices": 112,
"ngons": 0,
"materials": [
"LN_Crystal_Grotto"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
2.64,
0.5,
-3.4746
],
"max": [
3.6064,
2.7,
-2.7716
]
}
},
"Gate": {
"mesh": "Gate",
"triangles": 3944,
"vertices": 2016,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Grotto",
"LN_Metal_Bronze"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
3.2222,
-0.5,
-9.435
],
"max": [
9.435,
7.499,
-3.2222
]
}
},
"Cave": {
"mesh": "Cave",
"triangles": 424,
"vertices": 232,
"ngons": 0,
"materials": [
"LN_Cave",
"LN_Stone_Dark"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
4.575,
-1.93,
-10.0692
],
"max": [
10.0692,
3.7,
-4.575
]
}
},
"CaveGlow": {
"mesh": "CaveGlow",
"triangles": 182,
"vertices": 117,
"ngons": 0,
"materials": [
"LN_Water_Glow",
"LN_Crystal_Grotto"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
5.1204,
-0.97,
-9.4328
],
"max": [
9.5452,
2.7,
-5.4993
]
}
},
"Channel": {
"mesh": "Channel",
"triangles": 600,
"vertices": 304,
"ngons": 0,
"materials": [
"LN_Water_Glow"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.3712,
0.26,
-7.0092
],
"max": [
7.0092,
0.28,
0.3712
]
}
},
"Pillars": {
"mesh": "Pillars",
"triangles": 4328,
"vertices": 2280,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Grotto"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-7.7579,
-0.05,
-7.5321
],
"max": [
7.5454,
5.961,
7.7587
]
}
},
"PillarCrystals": {
"mesh": "PillarCrystals",
"triangles": 80,
"vertices": 48,
"ngons": 0,
"materials": [
"LN_Crystal_Grotto"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-6.4468,
5.23,
-7.0317
],
"max": [
7.014,
6.03,
6.441
]
}
},
"Rocks": {
"mesh": "Rocks",
"triangles": 800,
"vertices": 420,
"ngons": 0,
"materials": [
"LN_Rock"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-2.9737,
-0.6391,
-10.4646
],
"max": [
10.5442,
7.6927,
2.8772
]
}
}
},
"armatures": {},
"empties": {},
"bounds_unity": {
"min": [
-7.7775,
-1.93,
-10.4646
],
"max": [
10.5442,
7.6927,
7.7976
]
},
"clips": [],
"expected_from_godot": {
"triangles": 15182,
"bounds_godot": {
"min": [
-10.5442,
-1.93,
-10.4646
],
"max": [
7.7775,
7.6927,
7.7976
]
},
"resource_path": "res://assets/luneraria/g2/bossgrotte.glb"
},
"meshes_without_uv": [
"Cave",
"PillarCrystals",
"Rocks",
"Source"
],
"triangles": 15182,
"fbx": "Assets/Luneraria/Art/Buildings/bossgrotte.fbx",
"fbx_bytes": 989708,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b3c1a5924044546adb0dba298c3ddc35
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: 39a4b40e68ab84129824e427365b5868
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,456 @@
{
"id": "grenzwacht",
"group": "Buildings",
"source": "assets-source/luneraria/g2/grenzwacht.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['Crystal', 'Debris', 'Lower', 'Pennant', 'Ruin', 'Upper', 'Upper_Damaged']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "6516 fbx / 6516 godot"
},
{
"check": "mesh_names_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "materials_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_match_godot",
"ok": true,
"detail": "largest difference 0.00000 m"
},
{
"check": "no_armature",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "6516 / 6516"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
}
],
"source_sha256": "7067f4f3349918e412a3ee684651753bbb3ee958a46dd3775af6b93661d7e96f",
"removed_modifiers": 0,
"kind": "static",
"fps": 30.0,
"objects": 7,
"meshes": {
"Lower": {
"mesh": "Lower",
"triangles": 550,
"vertices": 296,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Pale",
"LN_Wood"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.3752,
0.0,
-1.3735
],
"max": [
1.3717,
2.3142,
1.3678
]
}
},
"Upper": {
"mesh": "Upper",
"triangles": 2536,
"vertices": 1336,
"ngons": 0,
"materials": [
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Rune_Team",
"LN_Stone_Dark",
"LN_Metal_Iron",
"LN_Wood"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.1568,
2.18,
-1.1568
],
"max": [
1.1568,
6.0601,
1.1568
]
}
},
"Upper_Damaged": {
"mesh": "Upper_Damaged",
"triangles": 2164,
"vertices": 1136,
"ngons": 0,
"materials": [
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Rune_Team",
"LN_Stone_Dark",
"LN_Wood",
"LN_Metal_Iron"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.1568,
2.18,
-1.1569
],
"max": [
1.1606,
6.0601,
1.1364
]
}
},
"Debris": {
"mesh": "Debris",
"triangles": 140,
"vertices": 84,
"ngons": 0,
"materials": [
"LN_Stone_Pale"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.0393,
-0.0358,
-1.4479
],
"max": [
1.8685,
0.2255,
0.2742
]
}
},
"Crystal": {
"mesh": "Crystal",
"triangles": 24,
"vertices": 14,
"ngons": 0,
"materials": [
"LN_Crystal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.3468,
4.7,
-0.3102
],
"max": [
0.345,
5.84,
0.3126
]
}
},
"Pennant": {
"mesh": "Pennant",
"triangles": 64,
"vertices": 45,
"ngons": 0,
"materials": [
"LN_Cloth_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
0.06,
4.72,
-1.02
],
"max": [
0.68,
5.72,
-1.02
]
}
},
"Ruin": {
"mesh": "Ruin",
"triangles": 1038,
"vertices": 580,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Pale",
"LN_Metal_Team",
"LN_Crystal_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-2.2575,
-0.0653,
-2.5526
],
"max": [
1.9658,
1.7462,
1.3683
]
}
}
},
"armatures": {},
"empties": {},
"bounds_unity": {
"min": [
-2.2575,
-0.0653,
-2.5526
],
"max": [
1.9658,
6.0601,
1.3683
]
},
"clips": [],
"expected_from_godot": {
"triangles": 6516,
"bounds_godot": {
"min": [
-1.3717,
0.0,
-1.3735
],
"max": [
1.3752,
6.0601,
1.3678
]
},
"resource_path": "res://assets/luneraria/g2/grenzwacht.glb"
},
"meshes_without_uv": [
"Crystal",
"Debris"
],
"triangles": 6516,
"fbx": "Assets/Luneraria/Art/Buildings/grenzwacht.fbx",
"fbx_bytes": 457404,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 81cb37d5386d048999e1ca915d0cb88d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: 5b76880584cb448b5b6c8a5f93db8b54
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,526 @@
{
"id": "kernwacht",
"group": "Buildings",
"source": "assets-source/luneraria/g2/kernwacht.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['Banner_L', 'Banner_R', 'Crystal', 'Debris', 'Lower', 'Ring', 'Ruin', 'Upper', 'Upper_Damaged']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "9224 fbx / 9224 godot"
},
{
"check": "mesh_names_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "materials_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_match_godot",
"ok": true,
"detail": "largest difference 0.00000 m"
},
{
"check": "no_armature",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "9224 / 9224"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
}
],
"source_sha256": "2797f6f7b705a99333dbfdfce029a2f4894a74d2fb7939eb108ab2eaf7edd09e",
"removed_modifiers": 0,
"kind": "static",
"fps": 30.0,
"objects": 9,
"meshes": {
"Lower": {
"mesh": "Lower",
"triangles": 548,
"vertices": 304,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Pale",
"LN_Rune_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.8139,
0.0,
-1.8017
],
"max": [
1.8045,
2.76,
1.806
]
}
},
"Upper": {
"mesh": "Upper",
"triangles": 3280,
"vertices": 1660,
"ngons": 0,
"materials": [
"LN_Metal_Team",
"LN_Crystal_Team",
"LN_Stone_Pale",
"LN_Metal_Iron"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.6442,
2.5345,
-1.0915
],
"max": [
1.6442,
8.33,
1.734
]
}
},
"Upper_Damaged": {
"mesh": "Upper_Damaged",
"triangles": 3280,
"vertices": 1660,
"ngons": 0,
"materials": [
"LN_Metal_Team",
"LN_Crystal_Team",
"LN_Stone_Pale",
"LN_Metal_Iron"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.6442,
2.5345,
-1.0915
],
"max": [
1.6442,
8.33,
1.734
]
}
},
"Debris": {
"mesh": "Debris",
"triangles": 160,
"vertices": 96,
"ngons": 0,
"materials": [
"LN_Stone_Pale"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-2.1991,
-0.0421,
-1.8567
],
"max": [
1.8296,
0.2606,
0.2916
]
}
},
"Crystal": {
"mesh": "Crystal",
"triangles": 24,
"vertices": 14,
"ngons": 0,
"materials": [
"LN_Crystal_Team"
],
"uv_layers": [],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.5554,
4.51,
-0.4884
],
"max": [
0.5682,
6.25,
0.5124
]
}
},
"Ring": {
"mesh": "Ring",
"triangles": 640,
"vertices": 320,
"ngons": 0,
"materials": [
"LN_Metal_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.03,
5.195,
-1.03
],
"max": [
1.03,
5.305,
1.03
]
}
},
"Banner_L": {
"mesh": "Banner_L",
"triangles": 80,
"vertices": 55,
"ngons": 0,
"materials": [
"LN_Cloth_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
1.5597,
3.9,
-1.2585
],
"max": [
1.8697,
5.5,
-0.7215
]
}
},
"Banner_R": {
"mesh": "Banner_R",
"triangles": 80,
"vertices": 55,
"ngons": 0,
"materials": [
"LN_Cloth_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.8697,
3.9,
-1.2585
],
"max": [
-1.5597,
5.5,
-0.7215
]
}
},
"Ruin": {
"mesh": "Ruin",
"triangles": 1132,
"vertices": 632,
"ngons": 0,
"materials": [
"LN_Stone_Dark",
"LN_Stone_Pale",
"LN_Rune_Team",
"LN_Crystal_Team"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-6.0762,
-2.3959,
-2.1939
],
"max": [
2.3962,
2.0939,
1.8126
]
}
}
},
"armatures": {},
"empties": {},
"bounds_unity": {
"min": [
-6.0762,
-2.3959,
-2.1939
],
"max": [
2.3962,
8.33,
1.8126
]
},
"clips": [],
"expected_from_godot": {
"triangles": 9224,
"bounds_godot": {
"min": [
-1.8045,
0.0,
-1.8017
],
"max": [
1.8139,
8.33,
1.806
]
},
"resource_path": "res://assets/luneraria/g2/kernwacht.glb"
},
"meshes_without_uv": [
"Crystal",
"Debris"
],
"triangles": 9224,
"fbx": "Assets/Luneraria/Art/Buildings/kernwacht.fbx",
"fbx_bytes": 732684,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5dc29c90a350446e480591bab7513b23
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: 2d354ddf8728c467da524d4c540efccd
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f870578f70b3a45ccbfa68cceccd2579
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,405 @@
{
"id": "funkenlanze",
"group": "Characters",
"source": "assets-source/phase18/funkenlanze.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['FunkenBody']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "24036 fbx / 24036 godot"
},
{
"check": "mesh_names_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_match_godot",
"ok": true,
"detail": "largest difference 0.00005 m"
},
{
"check": "one_armature",
"ok": true,
"detail": "['FunkenRig']"
},
{
"check": "single_root_bone",
"ok": true,
"detail": "['Root']"
},
{
"check": "root_bone_name",
"ok": true,
"detail": "['Root']"
},
{
"check": "bone_count_match_godot",
"ok": true,
"detail": "26 / 26"
},
{
"check": "bone_names_portable",
"ok": true,
"detail": "[]"
},
{
"check": "skinned_meshes_present",
"ok": true,
"detail": ""
},
{
"check": "clips_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "required_clips_present",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "24036 / 24036"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_bones",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_clips",
"ok": true,
"detail": "['ability', 'attack_a', 'attack_b', 'death', 'hit', 'idle', 'start', 'stop', 'walk']"
}
],
"source_sha256": "e21714e04a93f6266aae2edbf7db69c25d33b00429ebf62c01c6f832e18a771f",
"removed_modifiers": 0,
"joined_parts": 104,
"kind": "skinned",
"fps": 30.0,
"objects": 2,
"meshes": {
"FunkenBody": {
"mesh": "FunkenBody",
"triangles": 24036,
"vertices": 12226,
"ngons": 0,
"materials": [
"FL_Basalt",
"FL_BurnishedBronze",
"FL_Obsidian",
"FL_Ember",
"FL_Joint",
"FL_BronzeEdge",
"FL_Iron",
"FL_Crystal"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [
"SurfaceColor"
],
"active_color": "SurfaceColor",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": true,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.5652,
-0.0104,
-0.2945
],
"max": [
0.43,
2.0212,
1.435
]
}
}
},
"armatures": {
"FunkenRig": {
"bones": [
"Root",
"Pelvis",
"Spine",
"Chest",
"Neck",
"Head",
"Clavicle.L",
"UpperArm.L",
"Forearm.L",
"Hand.L",
"Index.L",
"Clavicle.R",
"UpperArm.R",
"Forearm.R",
"Hand.R",
"Index.R",
"Weapon",
"Bolt",
"Crystal",
"Chamber",
"Thigh.L",
"Shin.L",
"Foot.L",
"Thigh.R",
"Shin.R",
"Foot.R"
],
"roots": [
"Root"
]
}
},
"empties": {},
"bounds_unity": {
"min": [
-0.5652,
-0.0104,
-0.2945
],
"max": [
0.43,
2.0212,
1.435
]
},
"clips": [
{
"name": "ability",
"frames": [
0.0,
39.0
],
"seconds": 1.3,
"loop": false,
"contact_seconds": 0.9
},
{
"name": "attack_a",
"frames": [
0.0,
39.0
],
"seconds": 1.3,
"loop": false,
"contact_seconds": 0.9
},
{
"name": "attack_b",
"frames": [
0.0,
39.0
],
"seconds": 1.3,
"loop": false,
"contact_seconds": 0.9
},
{
"name": "death",
"frames": [
0.0,
60.0
],
"seconds": 2.0,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "hit",
"frames": [
0.0,
15.0
],
"seconds": 0.5,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "idle",
"frames": [
0.0,
90.0
],
"seconds": 3.0,
"loop": true,
"contact_seconds": -1.0
},
{
"name": "start",
"frames": [
0.0,
12.0
],
"seconds": 0.4,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "stop",
"frames": [
0.0,
12.0
],
"seconds": 0.4,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "walk",
"frames": [
0.0,
36.0
],
"seconds": 1.2,
"loop": true,
"contact_seconds": -1.0
}
],
"expected_from_godot": {
"triangles": 24036,
"bounds_godot": {
"min": [
-0.4300000071525574,
-0.010409347712993622,
-0.2945486903190613
],
"max": [
0.5651692152023315,
2.021195411682129,
1.4350000619888306
]
},
"bones": 26,
"clips": [
"idle",
"start",
"walk",
"stop",
"attack_a",
"attack_b",
"hit",
"ability",
"death"
],
"resource_path": "res://assets/characters/funkenlanze.glb"
},
"meshes_without_uv": [],
"triangles": 24036,
"fbx": "Assets/Luneraria/Art/Characters/funkenlanze.fbx",
"fbx_bytes": 3675308,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 373bb56c33f234adeac31c870ede9072
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,393 @@
fileFormatVersion: 2
guid: 652ee682a82c34ff99c2eb11cd218f5e
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations:
- serializedVersion: 16
name: ability
takeName: FunkenRig|ability
internalID: -1787258805998246360
firstFrame: 0
lastFrame: 39
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.6923077
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: attack_a
takeName: FunkenRig|attack_a
internalID: -6628820121952251911
firstFrame: 0
lastFrame: 39
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.6923077
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: attack_b
takeName: FunkenRig|attack_b
internalID: -2963360793056117044
firstFrame: 0
lastFrame: 39
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.6923077
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: death
takeName: FunkenRig|death
internalID: 9159963757730041032
firstFrame: 0
lastFrame: 60
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: hit
takeName: FunkenRig|hit
internalID: -4707179913997139171
firstFrame: 0
lastFrame: 15
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: idle
takeName: FunkenRig|idle
internalID: -1348917559068937310
firstFrame: 0
lastFrame: 90
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 1
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: start
takeName: FunkenRig|start
internalID: 6150794267048699607
firstFrame: 0
lastFrame: 12
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: stop
takeName: FunkenRig|stop
internalID: 637536857275861776
firstFrame: 0
lastFrame: 12
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: walk
takeName: FunkenRig|walk
internalID: 7607972658950073716
firstFrame: 0
lastFrame: 36
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 1
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 1
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 2
humanoidOversampling: 1
avatarSetup: 1
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,431 @@
{
"id": "schlundkrone",
"group": "Characters",
"source": "assets-source/luneraria/g2/schlundkrone.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['SchlundkroneBody']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "5712 fbx / 5712 godot"
},
{
"check": "materials_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_match_godot",
"ok": true,
"detail": "largest difference 0.00000 m"
},
{
"check": "one_armature",
"ok": true,
"detail": "['SchlundkroneRig']"
},
{
"check": "single_root_bone",
"ok": true,
"detail": "['Root']"
},
{
"check": "root_bone_name",
"ok": true,
"detail": "['Root']"
},
{
"check": "bone_names_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "bone_count_match_godot",
"ok": true,
"detail": "24 / 24"
},
{
"check": "bone_names_portable",
"ok": true,
"detail": "[]"
},
{
"check": "skinned_meshes_present",
"ok": true,
"detail": ""
},
{
"check": "clips_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "required_clips_present",
"ok": true,
"detail": "[]"
},
{
"check": "clip_lengths_match_godot",
"ok": true,
"detail": "largest difference 0.0000 s"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "5712 / 5712"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_bones",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_clips",
"ok": true,
"detail": "['ability', 'attack_a', 'attack_b', 'death', 'hit', 'idle', 'roar', 'start', 'stop', 'victory', 'walk']"
}
],
"source_sha256": "21f116f3ccf293b7f4c86bd23c07f86a41bd5d29c032f28bdb5c44043e9c6d7b",
"removed_modifiers": 0,
"kind": "skinned",
"fps": 30.0,
"objects": 2,
"meshes": {
"SchlundkroneBody": {
"mesh": "SchlundkroneBody",
"triangles": 5712,
"vertices": 3048,
"ngons": 0,
"materials": [
"LN_Boss_Hide",
"LN_Boss_Bone",
"LN_Crystal_Boss",
"LN_Boss_Bronze"
],
"uv_layers": [
"UVMap",
"UVRest"
],
"color_attributes": [
"Col"
],
"active_color": "Col",
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": true,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.3273,
0.0053,
-0.5532
],
"max": [
1.2307,
3.3182,
0.9253
]
}
}
},
"armatures": {
"SchlundkroneRig": {
"bones": [
"Root",
"Hips",
"Spine",
"Chest",
"Neck",
"Head",
"Jaw",
"Crown",
"Maw.L",
"Shoulder.L",
"UpperArm.L",
"Forearm.L",
"Hand.L",
"Maw.R",
"Shoulder.R",
"UpperArm.R",
"Forearm.R",
"Hand.R",
"Thigh.L",
"Shin.L",
"Foot.L",
"Thigh.R",
"Shin.R",
"Foot.R"
],
"roots": [
"Root"
]
}
},
"empties": {},
"bounds_unity": {
"min": [
-1.3273,
0.0053,
-0.5532
],
"max": [
1.2307,
3.3182,
0.9253
]
},
"clips": [
{
"name": "ability",
"frames": [
0.0,
39.0
],
"seconds": 1.3,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "attack_a",
"frames": [
0.0,
39.0
],
"seconds": 1.3,
"loop": false,
"contact_seconds": 0.9
},
{
"name": "attack_b",
"frames": [
0.0,
39.0
],
"seconds": 1.3,
"loop": false,
"contact_seconds": 0.9
},
{
"name": "death",
"frames": [
0.0,
66.0
],
"seconds": 2.2,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "hit",
"frames": [
0.0,
15.0
],
"seconds": 0.5,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "idle",
"frames": [
0.0,
90.0
],
"seconds": 3.0,
"loop": true,
"contact_seconds": -1.0
},
{
"name": "roar",
"frames": [
0.0,
60.0
],
"seconds": 2.0,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "start",
"frames": [
0.0,
12.0
],
"seconds": 0.4,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "stop",
"frames": [
0.0,
12.0
],
"seconds": 0.4,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "victory",
"frames": [
0.0,
60.0
],
"seconds": 2.0,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "walk",
"frames": [
0.0,
48.0
],
"seconds": 1.6,
"loop": true,
"contact_seconds": -1.0
}
],
"expected_from_godot": {
"triangles": 5712,
"bounds_godot": {
"min": [
-1.2307,
0.0053,
-0.5532
],
"max": [
1.3273,
3.3182,
0.9253
]
},
"bones": 24,
"clips": [
"ability",
"attack_a",
"attack_b",
"death",
"hit",
"idle",
"roar",
"start",
"stop",
"victory",
"walk"
],
"resource_path": "res://assets/luneraria/g2/schlundkrone.glb"
},
"meshes_without_uv": [],
"triangles": 5712,
"fbx": "Assets/Luneraria/Art/Characters/schlundkrone.fbx",
"fbx_bytes": 2838956,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 96a195571f91340e5b420cd273a91231
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,444 @@
fileFormatVersion: 2
guid: b2dd57798499c4b7b8698ee56e676c45
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations:
- serializedVersion: 16
name: ability
takeName: SchlundkroneRig|ability
internalID: 8816013596772264376
firstFrame: 0
lastFrame: 39
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: attack_a
takeName: SchlundkroneRig|attack_a
internalID: -3581854836903996656
firstFrame: 0
lastFrame: 39
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.6923077
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: attack_b
takeName: SchlundkroneRig|attack_b
internalID: 7137080732755112609
firstFrame: 0
lastFrame: 39
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.6923077
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: death
takeName: SchlundkroneRig|death
internalID: 3223440528613674463
firstFrame: 0
lastFrame: 66
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: hit
takeName: SchlundkroneRig|hit
internalID: -1153443838369333364
firstFrame: 0
lastFrame: 15
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: idle
takeName: SchlundkroneRig|idle
internalID: 2159908019584541609
firstFrame: 0
lastFrame: 90
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 1
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: roar
takeName: SchlundkroneRig|roar
internalID: 1662885906025357955
firstFrame: 0
lastFrame: 60
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: start
takeName: SchlundkroneRig|start
internalID: -8546801627699338383
firstFrame: 0
lastFrame: 12
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: stop
takeName: SchlundkroneRig|stop
internalID: 8440139490132052637
firstFrame: 0
lastFrame: 12
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: victory
takeName: SchlundkroneRig|victory
internalID: 7279649719363434968
firstFrame: 0
lastFrame: 60
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: walk
takeName: SchlundkroneRig|walk
internalID: -395723189595340628
firstFrame: 0
lastFrame: 48
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 1
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 1
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 2
humanoidOversampling: 1
avatarSetup: 1
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2cf6ff2112f7d4fd5bcff4710800659a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,684 @@
{
"id": "map_props",
"group": "Environment",
"source": "assets-source/luneraria/map_props.blend",
"exporter_version": 5,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": true,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['Banner', 'Bridge', 'Curb', 'EdgeCliff', 'Oak_0', 'Oak_1', 'Oak_2', 'Pine_0', 'Pine_1', 'Pine_2', 'Rock_0', 'Rock_1', 'Rock_2', 'Shrub_0', 'Shrub_1']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangent_prerequisites",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "no_armature",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "7338 / 7338"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
}
],
"source_sha256": "e2e723fc53da0d4bb686800ddba36ebebe7708e0944c32a81fd6d9c159e30e68",
"removed_modifiers": 0,
"kind": "static",
"fps": 24.0,
"objects": 15,
"meshes": {
"Bridge": {
"mesh": "Deck_0",
"triangles": 1088,
"vertices": 656,
"ngons": 0,
"materials": [
"ENV_paving",
"ENV_limestone",
"ENV_brass"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-5.515,
-1.3,
-3.9
],
"max": [
5.515,
1.46,
3.9
]
}
},
"Curb": {
"mesh": "Curb",
"triangles": 12,
"vertices": 8,
"ngons": 0,
"materials": [
"ENV_limestone"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.65,
-0.01,
-0.21
],
"max": [
0.65,
0.25,
0.21
]
}
},
"Pine_0": {
"mesh": "Trunk",
"triangles": 358,
"vertices": 191,
"ngons": 0,
"materials": [
"ENV_bark",
"ENV_leaf_dark",
"ENV_leaf_mid"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.411,
0.0,
-1.3761
],
"max": [
1.2882,
4.332,
1.3462
]
}
},
"Pine_1": {
"mesh": "Trunk.001",
"triangles": 358,
"vertices": 191,
"ngons": 0,
"materials": [
"ENV_bark",
"ENV_leaf_dark",
"ENV_leaf_mid"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.4442,
0.0,
-1.4865
],
"max": [
1.434,
5.244,
1.3943
]
}
},
"Pine_2": {
"mesh": "Trunk.002",
"triangles": 358,
"vertices": 191,
"ngons": 0,
"materials": [
"ENV_bark",
"ENV_leaf_dark",
"ENV_leaf_mid"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.5452,
0.0,
-1.5182
],
"max": [
1.4493,
3.648,
1.5733
]
}
},
"Oak_0": {
"mesh": "Trunk.003",
"triangles": 1270,
"vertices": 669,
"ngons": 0,
"materials": [
"ENV_bark",
"ENV_leaf_mid",
"ENV_leaf_light",
"ENV_leaf_dark"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.7039,
-0.0306,
-1.8912
],
"max": [
1.373,
4.02,
1.5742
]
}
},
"Oak_1": {
"mesh": "Trunk.004",
"triangles": 1270,
"vertices": 669,
"ngons": 0,
"materials": [
"ENV_bark",
"ENV_leaf_mid",
"ENV_leaf_light",
"ENV_leaf_dark"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.7964,
-0.0255,
-1.5792
],
"max": [
1.5389,
4.6442,
1.6028
]
}
},
"Oak_2": {
"mesh": "Trunk.005",
"triangles": 1270,
"vertices": 669,
"ngons": 0,
"materials": [
"ENV_bark",
"ENV_leaf_mid",
"ENV_leaf_light",
"ENV_leaf_dark"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.7448,
-0.0387,
-1.4185
],
"max": [
1.6053,
3.4947,
1.7902
]
}
},
"Rock_0": {
"mesh": "Rock",
"triangles": 140,
"vertices": 78,
"ngons": 0,
"materials": [
"ENV_cliff",
"ENV_leaf_dark",
"ENV_limestone"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.7155,
-0.04,
-1.0953
],
"max": [
1.0683,
1.0416,
1.0294
]
}
},
"Rock_1": {
"mesh": "Rock.001",
"triangles": 140,
"vertices": 78,
"ngons": 0,
"materials": [
"ENV_cliff",
"ENV_leaf_dark",
"ENV_limestone"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-2.4291,
-0.055,
-1.4554
],
"max": [
1.8589,
1.5017,
1.32
]
}
},
"Rock_2": {
"mesh": "Rock.002",
"triangles": 140,
"vertices": 78,
"ngons": 0,
"materials": [
"ENV_cliff",
"ENV_leaf_dark",
"ENV_limestone"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.2032,
-0.025,
-0.7362
],
"max": [
0.776,
0.6172,
0.7375
]
}
},
"Shrub_0": {
"mesh": "Shrub",
"triangles": 80,
"vertices": 42,
"ngons": 0,
"materials": [
"ENV_leaf_mid"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.813,
0.135,
-0.8861
],
"max": [
0.7211,
0.8205,
0.7093
]
}
},
"Shrub_1": {
"mesh": "Shrub.001",
"triangles": 80,
"vertices": 42,
"ngons": 0,
"materials": [
"ENV_leaf_dark"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.0772,
0.135,
-0.8513
],
"max": [
0.9518,
0.9641,
0.8895
]
}
},
"Banner": {
"mesh": "Pole",
"triangles": 74,
"vertices": 48,
"ngons": 0,
"materials": [
"ENV_brass",
"PR_Banner"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-1.13,
0.0,
-0.0866
],
"max": [
0.1,
4.5,
0.0866
]
}
},
"EdgeCliff": {
"mesh": "Block_0",
"triangles": 700,
"vertices": 378,
"ngons": 0,
"materials": [
"ENV_cliff",
"ENV_leaf_mid"
],
"uv_layers": [],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"shape_keys": 0,
"skinned": false,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-7.3684,
0.4308,
-2.0375
],
"max": [
7.4555,
4.1121,
1.9761
]
}
}
},
"armatures": {},
"empties": {},
"bounds_unity": {
"min": [
-7.3684,
-1.3,
-3.9
],
"max": [
7.4555,
5.244,
3.9
]
},
"clips": [],
"expected_from_godot": {},
"meshes_without_uv": [
"Banner",
"Bridge",
"Curb",
"EdgeCliff",
"Oak_0",
"Oak_1",
"Oak_2",
"Pine_0",
"Pine_1",
"Pine_2",
"Rock_0",
"Rock_1",
"Rock_2",
"Shrub_0",
"Shrub_1"
],
"triangles": 7338,
"fbx": "Assets/Luneraria/Art/Environment/map_props.fbx",
"fbx_bytes": 236284,
"ok": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 712d173cb82314d19a6d0689762cb0ef
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: 9a6862867d0c9498892d42d256fbe886
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 01c1714cee0714bbe9b65e1e6009cd0c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d7a4fb253a9584d0ba79fa033c20eb6b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View file

@ -0,0 +1,111 @@
fileFormatVersion: 2
guid: 416c29115e263420dbef5a876b74571c
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 1
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 0
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 0
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4460d353d8e864b71bbfb46336516b2d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 793ae230624f046acb4639e9b27fa0be
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json.Linq;
namespace Luneraria.EditorTools
{
/// <summary>
/// The facts and checks that tools/blender/lune_fbx_export.py writes next to every FBX
/// (&lt;id&gt;.export.json). The importer reads the clip data from it; the verifier compares
/// the imported model with the Blender side.
/// </summary>
public sealed class ExportReport
{
public readonly string Path;
public readonly JObject Data;
ExportReport(string path, JObject data)
{
Path = path;
Data = data;
}
public string Id { get { return (string)Data["id"]; } }
public string Kind { get { return (string)Data["kind"]; } }
public bool Skinned { get { return Kind == "skinned"; } }
public bool BlenderOk { get { return (bool)Data["ok"]; } }
public JObject Meshes { get { return (JObject)Data["meshes"]; } }
public JArray Clips { get { return (JArray)Data["clips"] ?? new JArray(); } }
/// <summary>Report of an FBX asset path, or null if the model was not exported by the pipeline.</summary>
public static ExportReport ForModel(string modelPath)
{
string path = System.IO.Path.ChangeExtension(modelPath, ".export.json");
if (!File.Exists(path))
return null;
return new ExportReport(path, JObject.Parse(File.ReadAllText(path)));
}
public static IEnumerable<string> AllModels(string root)
{
foreach (string path in Directory.GetFiles(root, "*.export.json", SearchOption.AllDirectories))
yield return path.Replace('\\', '/').Replace(".export.json", ".fbx");
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2ea3ae2e05fa54b17a8ef5eed98cad51

View file

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;
namespace Luneraria.EditorTools
{
/// <summary>
/// Renders every pipeline asset in a neutral preview scene (URP, one sun, 3/4 front view) to
/// docs/port/screenshots/u2a/. Buildings show their intact state; the tower also its ruin;
/// rigged figures their bind pose and the hit frame of attack_a.
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneAssetPreview.RenderAll
/// </summary>
public static class LuneAssetPreview
{
const int Width = 1600;
const int Height = 1000;
public static void RenderAll()
{
int code = 0;
try
{
string folder = Path.Combine(Path.GetDirectoryName(Application.dataPath), "docs/port/screenshots/u2a");
Directory.CreateDirectory(folder);
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
var sun = new GameObject("Sun").AddComponent<Light>();
sun.type = LightType.Directional;
sun.intensity = 1.6f;
sun.color = new Color(1f, .89f, .74f);
sun.shadows = LightShadows.Soft;
sun.transform.rotation = Quaternion.Euler(48f, 150f, 0f);
RenderSettings.ambientMode = AmbientMode.Trilight;
RenderSettings.ambientSkyColor = new Color(.55f, .6f, .68f);
RenderSettings.ambientEquatorColor = new Color(.42f, .42f, .44f);
RenderSettings.ambientGroundColor = new Color(.2f, .19f, .18f);
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.transform.localScale = new Vector3(20f, 1f, 20f);
var camera = new GameObject("PreviewCamera").AddComponent<Camera>();
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(.16f, .18f, .21f);
camera.fieldOfView = 30f;
foreach (string model in ExportReport.AllModels("Assets/Luneraria/Art").OrderBy(p => p, StringComparer.Ordinal))
{
ExportReport report = ExportReport.ForModel(model);
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(model);
foreach (var shot in Shots(report))
{
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
shot.Value(instance);
Render(camera, instance, Path.Combine(folder, report.Id + shot.Key + ".png"));
UnityEngine.Object.DestroyImmediate(instance);
}
}
}
catch (Exception error)
{
Debug.LogError("LUNE_PREVIEW failed: " + error);
code = 1;
}
EditorApplication.Exit(code);
}
static IEnumerable<KeyValuePair<string, Action<GameObject>>> Shots(ExportReport report)
{
string group = (string)report.Data["group"];
if (group == "Buildings")
{
yield return Shot("", go => Show(go, n => !n.StartsWith("Upper_Damaged") && !n.StartsWith("Debris") && !n.StartsWith("Ruin")));
if (report.Id == "grenzwacht")
yield return Shot("-ruin", go => Show(go, n => n.StartsWith("Ruin")));
}
else if (group == "Environment")
{
yield return Shot("", Row);
}
else if (report.Skinned)
{
yield return Shot("", go => Sample(go, "idle", 0f));
yield return Shot("-attack_a", go => Sample(go, "attack_a", .9f));
}
else
{
yield return Shot("", go => { });
}
}
static KeyValuePair<string, Action<GameObject>> Shot(string suffix, Action<GameObject> prepare)
{
return new KeyValuePair<string, Action<GameObject>>(suffix, prepare);
}
static void Show(GameObject root, Func<string, bool> visible)
{
foreach (Renderer renderer in root.GetComponentsInChildren<Renderer>(true))
renderer.enabled = visible(renderer.gameObject.name);
}
/// <summary>The prop library keeps every prop at the origin; the preview sets them out in a grid.</summary>
static void Row(GameObject root)
{
const int columns = 5;
const float cell = 7.5f;
int index = 0;
foreach (Transform child in root.transform.Cast<Transform>().OrderBy(t => t.name, StringComparer.Ordinal).ToList())
{
var renderer = child.GetComponent<Renderer>();
if (renderer == null)
continue;
Vector3 center = renderer.bounds.center;
var slot = new Vector3((index % columns) * cell, 0f, -(index / columns) * cell);
child.position += slot - new Vector3(center.x, 0f, center.z);
index++;
}
}
static void Sample(GameObject root, string clipName, float seconds)
{
string path = AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(root));
AnimationClip clip = AssetDatabase.LoadAllAssetsAtPath(path).OfType<AnimationClip>().FirstOrDefault(c => c.name == clipName);
if (clip != null)
clip.SampleAnimation(root, seconds);
}
static void Render(Camera camera, GameObject root, string file)
{
var renderers = root.GetComponentsInChildren<Renderer>().Where(r => r.enabled).ToList();
Bounds bounds = renderers[0].bounds;
foreach (Renderer renderer in renderers)
bounds.Encapsulate(renderer.bounds);
// 3/4 front view: the models face +Z, the camera stands in front, to the right, above.
Vector3 direction = new Vector3(.62f, .42f, 1f).normalized;
float radius = bounds.extents.magnitude;
float distance = radius / Mathf.Sin(camera.fieldOfView * .5f * Mathf.Deg2Rad) * .92f;
camera.aspect = (float)Width / Height;
camera.transform.position = bounds.center + direction * distance;
camera.transform.LookAt(bounds.center);
camera.nearClipPlane = Mathf.Max(.01f, distance - radius * 2f);
camera.farClipPlane = distance + radius * 4f;
var target = new RenderTexture(Width, Height, 24, RenderTextureFormat.ARGB32) { antiAliasing = 8 };
camera.targetTexture = target;
camera.Render();
RenderTexture.active = target;
var image = new Texture2D(Width, Height, TextureFormat.RGB24, false);
image.ReadPixels(new Rect(0, 0, Width, Height), 0, 0);
image.Apply();
File.WriteAllBytes(file, image.EncodeToPNG());
RenderTexture.active = null;
camera.targetTexture = null;
UnityEngine.Object.DestroyImmediate(target);
UnityEngine.Object.DestroyImmediate(image);
Debug.Log("LUNE_PREVIEW wrote " + file);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50634406ba3e34d398502bba3379f6af

View file

@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace Luneraria.EditorTools
{
/// <summary>
/// Checks every imported FBX of the Blender pipeline against the facts Blender wrote next to it
/// (U2A): transforms free of axis rotations, mesh names, triangles, bounds, material slots,
/// normals, tangents, UVs, vertex colours, root bone and bone names, clips with loop flags and
/// contact events, forward direction. Batch entry:
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneAssetVerifier.VerifyAll
/// Result: docs/port/logs/u2a/unity-verify.json; exit code 0 only if every check passed.
/// </summary>
public static class LuneAssetVerifier
{
const string Root = "Assets/Luneraria/Art";
const float BoundsTolerance = 0.002f;
/// <summary>Re-imports every pipeline model (after import rules or the render pipeline changed).</summary>
public static void ReimportArt()
{
AssetDatabase.ImportAsset(Root, ImportAssetOptions.ImportRecursive | ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
Debug.Log("LUNE_VERIFY reimported " + Root);
EditorApplication.Exit(0);
}
public static void VerifyAll()
{
int code = 1;
try
{
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
var results = new JArray();
bool all = true;
foreach (string model in ExportReport.AllModels(Root).OrderBy(p => p, StringComparer.Ordinal))
{
JObject result = Verify(model);
results.Add(result);
all &= (bool)result["ok"];
var failed = ((JArray)result["checks"]).Where(c => !(bool)c["ok"]).ToList();
Debug.Log(string.Format("LUNE_VERIFY {0} {1}: {2} checks, {3} failed{4}", failed.Count == 0 ? "OK" : "FAIL", result["id"],
((JArray)result["checks"]).Count, failed.Count, string.Concat(failed.Select(c => "\n FAIL " + c["check"] + ": " + c["detail"]))));
}
string logs = Path.Combine(Path.GetDirectoryName(Application.dataPath), "docs/port/logs/u2a");
Directory.CreateDirectory(logs);
var summary = new JObject { ["unity"] = Application.unityVersion, ["ok"] = all, ["assets"] = results };
File.WriteAllText(Path.Combine(logs, "unity-verify.json"), summary.ToString() + "\n");
code = all ? 0 : 2;
}
catch (Exception error)
{
Debug.LogError("LUNE_VERIFY crashed: " + error);
}
EditorApplication.Exit(code);
}
sealed class Checks
{
public readonly JArray List = new JArray();
public void Add(string name, bool ok, object detail = null)
{
List.Add(new JObject { ["check"] = name, ["ok"] = ok, ["detail"] = detail == null ? "" : JToken.FromObject(detail).ToString(Newtonsoft.Json.Formatting.None) });
}
public bool Ok { get { return List.All(c => (bool)c["ok"]); } }
}
public static JObject Verify(string modelPath)
{
ExportReport report = ExportReport.ForModel(modelPath);
var checks = new Checks();
var facts = new JObject();
var root = AssetDatabase.LoadAssetAtPath<GameObject>(modelPath);
checks.Add("blender_checks_passed", report.BlenderOk);
checks.Add("imported", root != null, modelPath);
if (root == null)
return Result(report, checks, facts);
var importer = (ModelImporter)AssetImporter.GetAtPath(modelPath);
checks.Add("import_rules_applied", importer.globalScale == 1f && importer.useFileScale && importer.bakeAxisConversion
&& importer.importNormals == ModelImporterNormals.Import && importer.importTangents == ModelImporterTangents.Import);
checks.Add("root_transform_identity", IsIdentity(root.transform), Describe(root.transform));
var bones = new HashSet<Transform>();
foreach (var skinned in root.GetComponentsInChildren<SkinnedMeshRenderer>(true))
foreach (Transform bone in skinned.bones)
bones.Add(bone);
// Blender objects carry no rotation or scale; after the axis bake every node that is not
// a bone must be clean, otherwise the conversion ended up in the hierarchy.
var rotated = root.GetComponentsInChildren<Transform>(true).Where(t => t != root.transform && !bones.Contains(t) && !IsClean(t)).Select(Describe).ToList();
checks.Add("node_transforms_axis_clean", rotated.Count == 0, rotated.Take(8).ToList());
var renderers = root.GetComponentsInChildren<Renderer>(true).Where(r => r is MeshRenderer || r is SkinnedMeshRenderer).ToList();
var names = renderers.Select(r => r.gameObject.name).OrderBy(n => n, StringComparer.Ordinal).ToList();
var expectedNames = report.Meshes.Properties().Select(p => p.Name).OrderBy(n => n, StringComparer.Ordinal).ToList();
checks.Add("mesh_names", names.SequenceEqual(expectedNames), names.Except(expectedNames).Concat(expectedNames.Except(names)).Take(12).ToList());
int triangles = 0;
var wrongTriangles = new List<string>();
var wrongMaterials = new List<string>();
var badNormals = new List<string>();
var badTangents = new List<string>();
var missingUv = new List<string>();
var missingColors = new List<string>();
var low = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
var high = new Vector3(float.MinValue, float.MinValue, float.MinValue);
int withoutTangents = 0;
foreach (Renderer renderer in renderers)
{
Mesh mesh = renderer is SkinnedMeshRenderer skinned ? skinned.sharedMesh : renderer.GetComponent<MeshFilter>().sharedMesh;
string name = renderer.gameObject.name;
var blender = (JObject)report.Meshes[name];
int count = mesh.triangles.Length / 3;
triangles += count;
if (blender != null && (int)blender["triangles"] != count)
wrongTriangles.Add(name + " " + count + "/" + blender["triangles"]);
if (blender != null)
{
var want = ((JArray)blender["materials"]).Select(m => (string)m).OrderBy(m => m, StringComparer.Ordinal).ToList();
var got = renderer.sharedMaterials.Select(m => m != null ? m.name : "<none>").OrderBy(m => m, StringComparer.Ordinal).ToList();
if (!want.SequenceEqual(got))
wrongMaterials.Add(name + ": " + string.Join(",", got) + " / " + string.Join(",", want));
bool hasUv = ((JArray)blender["uv_layers"]).Count > 0;
if (hasUv && mesh.uv.Length != mesh.vertexCount)
missingUv.Add(name);
if (((JArray)blender["color_attributes"]).Count > 0 && mesh.colors.Length != mesh.vertexCount)
missingColors.Add(name);
Vector4[] tangents = mesh.tangents;
if (hasUv)
{
if (tangents.Length != mesh.vertexCount || tangents.Any(t => Mathf.Abs(Mathf.Abs(t.w) - 1f) > 1e-3f))
badTangents.Add(name);
}
else if (tangents.Length == 0)
{
withoutTangents++;
}
}
Vector3[] normals = mesh.normals;
if (normals.Length != mesh.vertexCount || normals.Any(n => Mathf.Abs(n.magnitude - 1f) > 1e-3f))
badNormals.Add(name);
Matrix4x4 toRoot = root.transform.worldToLocalMatrix * renderer.transform.localToWorldMatrix;
foreach (Vector3 vertex in mesh.vertices)
{
Vector3 p = toRoot.MultiplyPoint3x4(vertex);
low = Vector3.Min(low, p);
high = Vector3.Max(high, p);
}
}
facts["triangles"] = triangles;
facts["renderers"] = renderers.Count;
facts["meshes_without_uv_and_tangents"] = withoutTangents;
checks.Add("triangles", triangles == (int)report.Data["triangles"] && wrongTriangles.Count == 0, triangles + " unity / " + report.Data["triangles"] + " blender " + string.Join("; ", wrongTriangles.Take(5)));
checks.Add("material_slots", wrongMaterials.Count == 0, wrongMaterials.Take(5).ToList());
checks.Add("normals_unit_length", badNormals.Count == 0, badNormals.Take(8).ToList());
checks.Add("tangents_where_uv", badTangents.Count == 0, badTangents.Take(8).ToList());
checks.Add("uvs_where_blender_has_uv", missingUv.Count == 0, missingUv.Take(8).ToList());
checks.Add("vertex_colors", missingColors.Count == 0, missingColors.Take(8).ToList());
var bounds = (JObject)report.Data["bounds_unity"];
if (bounds != null)
{
var wantLow = ToVector((JArray)bounds["min"]);
var wantHigh = ToVector((JArray)bounds["max"]);
float worst = Mathf.Max(MaxAbs(low - wantLow), MaxAbs(high - wantHigh));
facts["bounds_min"] = new JArray(low.x, low.y, low.z);
facts["bounds_max"] = new JArray(high.x, high.y, high.z);
checks.Add("bounds_axes_scale", worst < BoundsTolerance, string.Format("largest difference {0:0.00000} m; unity {1} {2}", worst, low.ToString("F3"), high.ToString("F3")));
}
JObject empties = (JObject)report.Data["empties"];
JProperty forward = empties?.Properties().FirstOrDefault(p => p.Name.StartsWith("Forward"));
if (forward != null)
{
Transform marker = root.GetComponentsInChildren<Transform>(true).FirstOrDefault(t => t.name.StartsWith("Forward"));
Vector3 at = marker != null ? root.transform.InverseTransformPoint(marker.position) : Vector3.zero;
checks.Add("forward_is_plus_z", marker != null && at.z > 0.1f && Mathf.Abs(at.x) < 1e-3f, at.ToString("F3"));
}
if (report.Skinned)
VerifyRig(modelPath, report, root, checks, facts);
return Result(report, checks, facts);
}
static void VerifyRig(string modelPath, ExportReport report, GameObject root, Checks checks, JObject facts)
{
var skinned = root.GetComponentsInChildren<SkinnedMeshRenderer>(true);
checks.Add("skinned_mesh", skinned.Length >= 1, skinned.Length);
var armature = (JObject)((JObject)report.Data["armatures"]).Properties().First().Value;
var wantBones = ((JArray)armature["bones"]).Select(b => (string)b).OrderBy(b => b, StringComparer.Ordinal).ToList();
string wantRoot = (string)((JArray)armature["roots"])[0];
foreach (SkinnedMeshRenderer renderer in skinned)
{
var got = renderer.bones.Select(b => b.name).Distinct().OrderBy(b => b, StringComparer.Ordinal).ToList();
checks.Add("bone_names " + renderer.name, got.SequenceEqual(wantBones), got.Except(wantBones).Concat(wantBones.Except(got)).ToList());
checks.Add("root_bone " + renderer.name, renderer.rootBone != null && renderer.rootBone.name == wantRoot, renderer.rootBone != null ? renderer.rootBone.name : "none");
}
UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(modelPath);
var avatar = assets.OfType<Avatar>().FirstOrDefault();
checks.Add("generic_avatar", avatar != null && avatar.isValid && !avatar.isHuman, avatar != null ? avatar.name : "none");
var clips = assets.OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview__")).ToDictionary(c => c.name, c => c);
var wrong = new List<string>();
foreach (JToken clip in report.Clips)
{
string name = (string)clip["name"];
if (!clips.TryGetValue(name, out AnimationClip unity))
{
wrong.Add(name + " missing");
continue;
}
float seconds = (float)clip["seconds"];
float frame = 1f / (float)report.Data["fps"];
if (Mathf.Abs(unity.length - seconds) > frame * 0.5f)
wrong.Add(string.Format("{0} length {1:0.000}/{2:0.000}", name, unity.length, seconds));
if (unity.isLooping != (bool)clip["loop"])
wrong.Add(name + " loop " + unity.isLooping);
double contact = (double)clip["contact_seconds"];
AnimationEvent[] events = AnimationUtility.GetAnimationEvents(unity);
bool hasContact = events.Any(e => e.functionName == LuneModelPostprocessor.ContactEvent && Mathf.Abs(e.time - (float)contact) < frame);
if (contact > 0 && !hasContact)
wrong.Add(name + " contact event");
}
var extra = clips.Keys.Except(report.Clips.Select(c => (string)c["name"])).ToList();
checks.Add("clips", wrong.Count == 0 && extra.Count == 0, wrong.Concat(extra.Select(e => e + " unexpected")).ToList());
facts["clips"] = new JArray(clips.Values.OrderBy(c => c.name).Select(c => new JObject { ["name"] = c.name, ["length"] = c.length, ["loop"] = c.isLooping, ["events"] = AnimationUtility.GetAnimationEvents(c).Length }));
}
static JObject Result(ExportReport report, Checks checks, JObject facts)
{
return new JObject { ["id"] = report.Id, ["kind"] = report.Kind, ["ok"] = checks.Ok, ["checks"] = checks.List, ["facts"] = facts };
}
static bool IsIdentity(Transform t)
{
return t.localPosition.sqrMagnitude < 1e-10f && IsClean(t);
}
static bool IsClean(Transform t)
{
return Quaternion.Angle(t.localRotation, Quaternion.identity) < 0.01f && (t.localScale - Vector3.one).sqrMagnitude < 1e-8f;
}
static string Describe(Transform t)
{
return string.Format("{0} pos {1} rot {2} scale {3}", t.name, t.localPosition.ToString("F4"), t.localEulerAngles.ToString("F2"), t.localScale.ToString("F4"));
}
static Vector3 ToVector(JArray values)
{
return new Vector3((float)values[0], (float)values[1], (float)values[2]);
}
static float MaxAbs(Vector3 v)
{
return Mathf.Max(Mathf.Abs(v.x), Mathf.Max(Mathf.Abs(v.y), Mathf.Abs(v.z)));
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 588d50b5a3531448691e7370abd8dc6e

View file

@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Luneraria.EditorTools
{
/// <summary>
/// Import rules for every FBX of the Blender pipeline under Assets/Luneraria/Art (U2A).
/// Metres stay metres, Blender's Z-up axes are converted by baking (no root rotation),
/// normals and tangents come from the file, rigs are generic with the Blender bone names,
/// and clips get their Blender action names, loop flags and contact events.
/// </summary>
public sealed class LuneModelPostprocessor : AssetPostprocessor
{
public const string ArtRoot = "Assets/Luneraria/Art/";
/// <summary>Raised on the figure at the hit frame of attack and ability clips.</summary>
public const string ContactEvent = "OnLuneContact";
public override uint GetVersion() { return 3; }
void OnPreprocessModel()
{
if (!assetPath.StartsWith(ArtRoot) || !assetPath.EndsWith(".fbx"))
return;
ExportReport report = ExportReport.ForModel(assetPath);
var importer = (ModelImporter)assetImporter;
importer.globalScale = 1f;
importer.useFileScale = true;
importer.bakeAxisConversion = true;
importer.importCameras = false;
importer.importLights = false;
importer.importVisibility = false;
importer.importBlendShapes = false;
importer.importNormals = ModelImporterNormals.Import;
importer.importTangents = ModelImporterTangents.Import;
importer.meshCompression = ModelImporterMeshCompression.Off;
importer.isReadable = true;
importer.materialImportMode = ModelImporterMaterialImportMode.ImportViaMaterialDescription;
importer.materialLocation = ModelImporterMaterialLocation.InPrefab;
bool skinned = report != null && report.Skinned;
importer.animationType = skinned ? ModelImporterAnimationType.Generic : ModelImporterAnimationType.None;
importer.avatarSetup = skinned ? ModelImporterAvatarSetup.CreateFromThisModel : ModelImporterAvatarSetup.NoAvatar;
importer.importAnimation = skinned;
importer.optimizeGameObjects = false;
importer.animationCompression = ModelImporterAnimationCompression.Off;
importer.resampleCurves = false;
}
void OnPreprocessAnimation()
{
if (!assetPath.StartsWith(ArtRoot) || !assetPath.EndsWith(".fbx"))
return;
ExportReport report = ExportReport.ForModel(assetPath);
if (report == null || !report.Skinned)
return;
var importer = (ModelImporter)assetImporter;
var blender = report.Clips.ToDictionary(c => (string)c["name"], c => c);
var clips = new List<ModelImporterClipAnimation>();
foreach (ModelImporterClipAnimation clip in importer.defaultClipAnimations)
{
// Blender names takes "<armature>|<action>"; Unity keeps the action name.
string name = clip.takeName.Contains("|") ? clip.takeName.Substring(clip.takeName.LastIndexOf('|') + 1) : clip.takeName;
clip.name = name;
if (blender.TryGetValue(name, out var facts))
{
clip.loopTime = (bool)facts["loop"];
double contact = (double)facts["contact_seconds"];
double seconds = (double)facts["seconds"];
if (contact > 0.0 && seconds > 0.0)
clip.events = new[] { new AnimationEvent { functionName = ContactEvent, time = (float)(contact / seconds) } };
}
clips.Add(clip);
}
importer.clipAnimations = clips.ToArray();
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3eb06f326c30446a4a9e3e2be3aee948

View file

@ -0,0 +1,22 @@
{
"name": "Luneraria.Editor",
"rootNamespace": "Luneraria.EditorTools",
"references": [
"Luneraria.Core",
"Unity.RenderPipelines.Core.Runtime",
"Unity.RenderPipelines.Universal.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"Newtonsoft.Json.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f6cff9d287b764627a17a7e3c7b99a6b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 695e40eeb3cef4a898659650d64902e2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,80 @@
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace Luneraria.EditorTools
{
/// <summary>
/// Rendering setup of the project (URP asset and renderer, linear colour space), idempotent.
/// Unity -batchmode -projectPath . -executeMethod Luneraria.EditorTools.LuneProjectSetup.ConfigureRendering
/// </summary>
public static class LuneProjectSetup
{
public const string SettingsFolder = "Assets/Luneraria/Settings/Rendering";
public const string PipelinePath = SettingsFolder + "/Luneraria_URP.asset";
public const string RendererPath = SettingsFolder + "/Luneraria_URP_Renderer.asset";
public static void ConfigureRendering()
{
int code = 0;
try
{
Configure();
}
catch (System.Exception error)
{
Debug.LogError("LUNE_SETUP failed: " + error);
code = 1;
}
EditorApplication.Exit(code);
}
public static UniversalRenderPipelineAsset Configure()
{
Directory.CreateDirectory(SettingsFolder);
AssetDatabase.Refresh();
var asset = AssetDatabase.LoadAssetAtPath<UniversalRenderPipelineAsset>(PipelinePath);
if (asset == null)
{
var renderer = ScriptableObject.CreateInstance<UniversalRendererData>();
AssetDatabase.CreateAsset(renderer, RendererPath);
ResourceReloader.ReloadAllNullIn(renderer, UniversalRenderPipelineAsset.packagePath);
asset = UniversalRenderPipelineAsset.Create(renderer);
AssetDatabase.CreateAsset(asset, PipelinePath);
}
// The Godot sun reaches 190 m with four splits (scripts/luneraria/view/lune_world.gd).
asset.shadowDistance = 190f;
asset.shadowCascadeCount = 4;
asset.supportsHDR = true;
asset.msaaSampleCount = 4;
asset.useSRPBatcher = true;
EditorUtility.SetDirty(asset);
GraphicsSettings.defaultRenderPipeline = asset;
int current = QualitySettings.GetQualityLevel();
for (int level = 0; level < QualitySettings.names.Length; level++)
{
QualitySettings.SetQualityLevel(level, false);
QualitySettings.renderPipeline = asset;
}
QualitySettings.SetQualityLevel(current, false);
PlayerSettings.colorSpace = ColorSpace.Linear;
MoveInto("Assets/UniversalRenderPipelineGlobalSettings.asset", SettingsFolder + "/UniversalRenderPipelineGlobalSettings.asset");
MoveInto("Assets/DefaultVolumeProfile.asset", SettingsFolder + "/DefaultVolumeProfile.asset");
AssetDatabase.SaveAssets();
Debug.Log("LUNE_SETUP rendering: " + PipelinePath + ", colour space " + PlayerSettings.colorSpace);
return asset;
}
static void MoveInto(string from, string to)
{
if (File.Exists(from) && !File.Exists(to))
{
string error = AssetDatabase.MoveAsset(from, to);
if (!string.IsNullOrEmpty(error))
Debug.LogWarning("LUNE_SETUP move " + from + ": " + error);
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e7bff4280437a4ab7bbe7bcaef5cf486

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9ca8da48623284af8ad6bdfdb0af7a1a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7d08bc5b72ee04727bf259d3ad7f45e0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
m_Name: Luneraria_URP
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset
k_AssetVersion: 13
k_AssetPreviousVersion: 13
m_RendererType: 1
m_RendererData: {fileID: 0}
m_RendererDataList:
- {fileID: 11400000, guid: 4f1d1577e34314798a12f2f2556faaec, type: 2}
m_DefaultRendererIndex: 0
m_RequireDepthTexture: 0
m_RequireOpaqueTexture: 0
m_OpaqueDownsampling: 1
m_SupportsTerrainHoles: 1
m_SupportsHDR: 1
m_HDRColorBufferPrecision: 0
m_MSAA: 4
m_RenderScale: 1
m_UpscalingFilter: 0
m_FsrOverrideSharpness: 0
m_FsrSharpness: 0.92
m_EnableLODCrossFade: 1
m_LODCrossFadeDitheringType: 1
m_ShEvalMode: 0
m_LightProbeSystem: 0
m_ProbeVolumeMemoryBudget: 1024
m_ProbeVolumeBlendingMemoryBudget: 256
m_SupportProbeVolumeGPUStreaming: 0
m_SupportProbeVolumeDiskStreaming: 0
m_SupportProbeVolumeScenarios: 0
m_SupportProbeVolumeScenarioBlending: 0
m_ProbeVolumeSHBands: 1
m_MainLightRenderingMode: 1
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 2048
m_AdditionalLightsRenderingMode: 1
m_AdditionalLightsPerObjectLimit: 4
m_AdditionalLightShadowsSupported: 0
m_AdditionalLightsShadowmapResolution: 2048
m_AdditionalLightsShadowResolutionTierLow: 256
m_AdditionalLightsShadowResolutionTierMedium: 512
m_AdditionalLightsShadowResolutionTierHigh: 1024
m_ReflectionProbeBlending: 0
m_ReflectionProbeBoxProjection: 0
m_ReflectionProbeAtlas: 1
m_ShadowDistance: 190
m_ShadowCascadeCount: 4
m_Cascade2Split: 0.25
m_Cascade3Split: {x: 0.1, y: 0.3}
m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467}
m_CascadeBorder: 0.2
m_ShadowDepthBias: 1
m_ShadowNormalBias: 1
m_AnyShadowsSupported: 1
m_SoftShadowsSupported: 0
m_ConservativeEnclosingSphere: 1
m_NumIterationsEnclosingSphere: 64
m_SoftShadowQuality: 2
m_AdditionalLightsCookieResolution: 2048
m_AdditionalLightsCookieFormat: 3
m_UseSRPBatcher: 1
m_SupportsDynamicBatching: 0
m_MixedLightingSupported: 1
m_SupportsLightCookies: 1
m_SupportsLightLayers: 0
m_DebugLevel: 0
m_StoreActionsOptimization: 0
m_UseAdaptivePerformance: 1
m_ColorGradingMode: 0
m_ColorGradingLutSize: 32
m_AllowPostProcessAlphaOutput: 0
m_UseFastSRGBLinearConversion: 0
m_SupportDataDrivenLensFlare: 1
m_SupportScreenSpaceLensFlare: 1
m_GPUResidentDrawerMode: 0
m_SmallMeshScreenPercentage: 0
m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0
m_ShadowType: 1
m_LocalShadowsSupported: 0
m_LocalShadowsAtlasResolution: 256
m_MaxPixelLights: 0
m_ShadowAtlasResolution: 256
m_VolumeFrameworkUpdateMode: 0
m_VolumeProfile: {fileID: 0}
apvScenesData:
obsoleteSceneBounds:
m_Keys: []
m_Values: []
obsoleteHasProbeVolumes:
m_Keys: []
m_Values:
m_PrefilteringModeMainLightShadows: 1
m_PrefilteringModeAdditionalLight: 4
m_PrefilteringModeAdditionalLightShadows: 1
m_PrefilterXRKeywords: 0
m_PrefilteringModeForwardPlus: 1
m_PrefilteringModeDeferredRendering: 1
m_PrefilteringModeScreenSpaceOcclusion: 1
m_PrefilterDebugKeywords: 0
m_PrefilterWriteRenderingLayers: 0
m_PrefilterHDROutput: 0
m_PrefilterAlphaOutput: 0
m_PrefilterSSAODepthNormals: 0
m_PrefilterSSAOSourceDepthLow: 0
m_PrefilterSSAOSourceDepthMedium: 0
m_PrefilterSSAOSourceDepthHigh: 0
m_PrefilterSSAOInterleaved: 0
m_PrefilterSSAOBlueNoise: 0
m_PrefilterSSAOSampleCountLow: 0
m_PrefilterSSAOSampleCountMedium: 0
m_PrefilterSSAOSampleCountHigh: 0
m_PrefilterDBufferMRT1: 0
m_PrefilterDBufferMRT2: 0
m_PrefilterDBufferMRT3: 0
m_PrefilterSoftShadowsQualityLow: 0
m_PrefilterSoftShadowsQualityMedium: 0
m_PrefilterSoftShadowsQualityHigh: 0
m_PrefilterSoftShadows: 0
m_PrefilterScreenCoord: 0
m_PrefilterScreenSpaceIrradiance: 0
m_PrefilterNativeRenderPass: 0
m_PrefilterUseLegacyLightmaps: 0
m_PrefilterBicubicLightmapSampling: 0
m_PrefilterReflectionProbeRotation: 0
m_PrefilterReflectionProbeBlending: 0
m_PrefilterReflectionProbeBoxProjection: 0
m_PrefilterReflectionProbeAtlas: 0
m_ShaderVariantLogLevel: 0
m_ShadowCascades: 0
m_Textures:
blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 88b2e99a809584b278f95d7e2a3dc890
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,55 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3}
m_Name: Luneraria_URP_Renderer
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRendererData
debugShaders:
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
probeVolumeResources:
probeVolumeDebugShader: {fileID: 0}
probeVolumeFragmentationDebugShader: {fileID: 0}
probeVolumeOffsetDebugShader: {fileID: 0}
probeVolumeSamplingDebugShader: {fileID: 0}
probeSamplingDebugMesh: {fileID: 0}
probeSamplingDebugTexture: {fileID: 0}
probeVolumeBlendStatesCS: {fileID: 0}
m_RendererFeatures: []
m_RendererFeatureMap:
m_UseNativeRenderPass: 0
postProcessData: {fileID: 0}
m_AssetVersion: 3
m_PrepassLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_TransparentLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_DefaultStencilState:
overrideStencilState: 0
stencilReference: 0
stencilCompareFunction: 8
passOperation: 2
failOperation: 0
zFailOperation: 0
m_ShadowTransparentReceive: 1
m_RenderingMode: 0
m_DepthPrimingMode: 0
m_CopyDepthMode: 1
m_DepthAttachmentFormat: 0
m_DepthTextureFormat: 0
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 1

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4f1d1577e34314798a12f2f2556faaec
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -37,7 +37,7 @@ GraphicsSettings:
m_PreloadedShaders: [] m_PreloadedShaders: []
m_PreloadShadersBatchTimeLimit: -1 m_PreloadShadersBatchTimeLimit: -1
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
m_CustomRenderPipeline: {fileID: 0} m_CustomRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
m_TransparencySortMode: 0 m_TransparencySortMode: 0
m_TransparencySortAxis: {x: 0, y: 0, z: 1} m_TransparencySortAxis: {x: 0, y: 0, z: 1}
m_DefaultRenderingPath: 1 m_DefaultRenderingPath: 1
@ -61,8 +61,8 @@ GraphicsSettings:
UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 6f0ce73cdc8074fc283478fba3d0ede7, type: 2} UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 6f0ce73cdc8074fc283478fba3d0ede7, type: 2}
m_ShaderBuildSettings: m_ShaderBuildSettings:
keywordDeclarationOverrides: [] keywordDeclarationOverrides: []
m_LightsUseLinearIntensity: 0 m_LightsUseLinearIntensity: 1
m_LightsUseColorTemperature: 0 m_LightsUseColorTemperature: 1
m_LogWhenShaderIsCompiled: 0 m_LogWhenShaderIsCompiled: 0
m_LightProbeOutsideHullStrategy: 1 m_LightProbeOutsideHullStrategy: 1
m_CameraRelativeLightCulling: 0 m_CameraRelativeLightCulling: 0

View file

@ -47,7 +47,7 @@ PlayerSettings:
defaultScreenWidthWeb: 960 defaultScreenWidthWeb: 960
defaultScreenHeightWeb: 600 defaultScreenHeightWeb: 600
m_StereoRenderingPath: 0 m_StereoRenderingPath: 0
m_ActiveColorSpace: 0 m_ActiveColorSpace: 1
unsupportedMSAAFallback: 0 unsupportedMSAAFallback: 0
m_SpriteBatchMaxVertexCount: 65535 m_SpriteBatchMaxVertexCount: 65535
m_SpriteBatchVertexThreshold: 300 m_SpriteBatchVertexThreshold: 300
@ -160,14 +160,18 @@ PlayerSettings:
useHDRDisplay: 0 useHDRDisplay: 0
hdrBitDepth: 0 hdrBitDepth: 0
m_ColorGamuts: 00000000 m_ColorGamuts: 00000000
targetPixelDensity: 0 targetPixelDensity: 30
resolutionScalingMode: 0 resolutionScalingMode: 0
resetResolutionOnWindowResize: 0 resetResolutionOnWindowResize: 0
androidSupportedAspectRatio: 1 androidSupportedAspectRatio: 1
androidMaxAspectRatio: 2.4 androidMaxAspectRatio: 2.4
androidMinAspectRatio: 1 androidMinAspectRatio: 1
applicationIdentifier: {} applicationIdentifier: {}
buildNumber: {} buildNumber:
Standalone: 0
VisionOS: 0
iPhone: 0
tvOS: 0
overrideDefaultApplicationIdentifier: 0 overrideDefaultApplicationIdentifier: 0
AndroidBundleVersionCode: 1 AndroidBundleVersionCode: 1
AndroidMinSdkVersion: 25 AndroidMinSdkVersion: 25
@ -188,13 +192,13 @@ PlayerSettings:
VertexChannelCompressionMask: 4054 VertexChannelCompressionMask: 4054
iPhoneSdkVersion: 988 iPhoneSdkVersion: 988
iOSSimulatorArchitecture: 0 iOSSimulatorArchitecture: 0
iOSTargetOSVersionString: iOSTargetOSVersionString: 15.0
tvOSSdkVersion: 0 tvOSSdkVersion: 0
tvOSSimulatorArchitecture: 0 tvOSSimulatorArchitecture: 0
tvOSRequireExtendedGameController: 0 tvOSRequireExtendedGameController: 0
tvOSTargetOSVersionString: tvOSTargetOSVersionString: 15.0
VisionOSSdkVersion: 0 VisionOSSdkVersion: 0
VisionOSTargetOSVersionString: VisionOSTargetOSVersionString: 1.0
uIPrerenderedIcon: 0 uIPrerenderedIcon: 0
uIRequiresPersistentWiFi: 0 uIRequiresPersistentWiFi: 0
uIRequiresFullScreen: 1 uIRequiresFullScreen: 1
@ -326,7 +330,7 @@ PlayerSettings:
locationUsageDescription: locationUsageDescription:
microphoneUsageDescription: microphoneUsageDescription:
bluetoothUsageDescription: bluetoothUsageDescription:
macOSTargetOSVersion: macOSTargetOSVersion: 12.0
switchNMETAOverride: switchNMETAOverride:
switchNetLibKey: switchNetLibKey:
switchSocketMemoryPoolSize: 6144 switchSocketMemoryPoolSize: 6144

View file

@ -48,7 +48,7 @@ QualitySettings:
asyncUploadBufferSize: 16 asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0} customRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
terrainQualityOverrides: 0 terrainQualityOverrides: 0
terrainPixelError: 1 terrainPixelError: 1
terrainDetailDensityScale: 1 terrainDetailDensityScale: 1
@ -101,7 +101,7 @@ QualitySettings:
asyncUploadBufferSize: 16 asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0} customRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
terrainQualityOverrides: 0 terrainQualityOverrides: 0
terrainPixelError: 1 terrainPixelError: 1
terrainDetailDensityScale: 1 terrainDetailDensityScale: 1
@ -154,7 +154,7 @@ QualitySettings:
asyncUploadBufferSize: 16 asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0} customRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
terrainQualityOverrides: 0 terrainQualityOverrides: 0
terrainPixelError: 1 terrainPixelError: 1
terrainDetailDensityScale: 1 terrainDetailDensityScale: 1
@ -207,7 +207,7 @@ QualitySettings:
asyncUploadBufferSize: 16 asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0} customRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
terrainQualityOverrides: 0 terrainQualityOverrides: 0
terrainPixelError: 1 terrainPixelError: 1
terrainDetailDensityScale: 1 terrainDetailDensityScale: 1
@ -260,7 +260,7 @@ QualitySettings:
asyncUploadBufferSize: 16 asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0} customRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
terrainQualityOverrides: 0 terrainQualityOverrides: 0
terrainPixelError: 1 terrainPixelError: 1
terrainDetailDensityScale: 1 terrainDetailDensityScale: 1
@ -287,7 +287,7 @@ QualitySettings:
globalTextureMipmapLimit: 0 globalTextureMipmapLimit: 0
textureMipmapLimitSettings: [] textureMipmapLimitSettings: []
anisotropicTextures: 2 anisotropicTextures: 2
antiAliasing: 2 antiAliasing: 4
softParticles: 1 softParticles: 1
softVegetation: 1 softVegetation: 1
realtimeReflectionProbes: 1 realtimeReflectionProbes: 1
@ -313,7 +313,7 @@ QualitySettings:
asyncUploadBufferSize: 16 asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1 asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1 resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0} customRenderPipeline: {fileID: 11400000, guid: 88b2e99a809584b278f95d7e2a3dc890, type: 2}
terrainQualityOverrides: 0 terrainQualityOverrides: 0
terrainPixelError: 1 terrainPixelError: 1
terrainDetailDensityScale: 1 terrainDetailDensityScale: 1

View file

@ -1,12 +1,19 @@
# U2 · Unity-Spielwelt # U2 · Unity-Spielwelt
Stand: 30. September 2026 · **nicht begonnen**. Stand: 30. September 2026.
Voraussetzungen: U1 grün (erfüllt außerhalb von Unity) und aktive Unity-Lizenz (fehlt). | Teil | Inhalt | Zustand |
| --- | --- | --- |
| U2A | Blender→FBX-Pipeline, sechs repräsentative Assets vollständig geprüft | **fertig** |
| U2B | Weltgeometrie (G1-Karte, Türme, Ancients, Grotte, Aufstellungsbereiche), Materialsystem | offen |
| U2C | Simulation/Darstellung: fester Takt, Interpolation, Bildraten-Unabhängigkeit | offen |
| U2D | spielbare Unity-Partie, Vertical Function Test | offen |
Voraussetzung U1.5 ist erfüllt ([U1.5-UNITY.md](U1.5-UNITY.md)).
## Ziel ## Ziel
Die bestehende Partie läuft sichtbar in Unity (URP). Das ist noch keine grafische Neugestaltung. Die bestehende Partie läuft sichtbar in Unity (URP). Das ist noch keine grafische Neugestaltung; die kommt erst mit U5.
Zuerst kommen: Zuerst kommen:
- die G1-Drei-Lane-Karte, - die G1-Drei-Lane-Karte,
@ -17,35 +24,127 @@ Zuerst kommen:
## Vorgaben ## Vorgaben
- **Adapter:** Unity treibt `LuneMatch` im festen Takt (0,05 s) über einen Akkumulator, unabhängig von der Bildrate. Ansichten lesen nur; sie schreiben nie in die Regeln. - **Adapter:** Unity treibt `LuneMatch` im festen Takt (0,05 s) über einen Akkumulator, unabhängig von der Bildrate. Ansichten lesen nur; sie schreiben nie in die Regeln. Beispiel: `Gebäude-HP = 0` führt zu Einsturz und Ruine, nie umgekehrt eine Animation zu einer Regelwirkung.
- **Gemeinsames Materialsystem** (URP, Shader Graph oder HLSL): Stein, Metall, Holz, Vegetation, Kristall, Runen, Wasser. Die G2-Shader aus Godot (stone, metal, crystal, rune, cloth, vein, water_glow, hide) dienen als fachliche Vorlage. Automatische Materialkonvertierungen werden nicht ungeprüft übernommen. - **Gemeinsames Materialsystem** (URP): Stein, Metall, Holz, Vegetation, Kristall, Runen, Wasser. Die G2-Shader aus Godot (stone, metal, crystal, rune, cloth, vein, water_glow, hide) dienen als fachliche Vorlage.
- **Assets:** Quelle sind die Blender-Dateien und Generatoren im Referenz-Repository. Ein zentraler, reproduzierbarer Blender-Export erzeugt FBX für Unity; GLB nur, wo keine bearbeitbare Quelle existiert. - **Assets:** Quelle sind die Blender-Dateien und Generatoren im Referenz-Repository. Ein zentraler, reproduzierbarer Blender-Export erzeugt FBX; GLB nur, wo keine bearbeitbare Quelle existiert.
- **Reihenfolge:** Erst sechs repräsentative Assets vollständig prüfen, dann die Welt und eine vollständige Bot-Partie. Den Batchlauf über alle Assets gibt es erst nach dem Vertical Function Test.
## Bestand an Quellen (Godot-Referenz) ## U2A · Blender→FBX-Pipeline
| Gruppe | GLB | Blender-Quelle | ### Aufbau
| --- | ---: | --- |
| Truppen, Monster, Sendungen (`assets/models`) | 59 | `assets-source/phase4/blend` (59 `.blend`) |
| Figuren mit Skelett (`assets/characters`) | 6 | `assets-source/phase10`–`phase19` (je ein `.blend` plus Generator) |
| G2-Bauwerke, Grotte, Schlundkrone (`assets/luneraria/g2`) | 6 | `assets-source/luneraria/g2` (`.blend` plus Generatoren) |
| Völker (`assets/luneraria/peoples`) | 7 | `assets-source/luneraria/peoples` |
| Umgebung, Vergleich, übrige | 11 | teilweise; wird im Exportinventar geklärt |
## Prüfpunkte der Pipeline | Datei | Aufgabe |
| --- | --- |
| `tools/blender/export_manifest.json` | Asset-Liste: Blender-Quelle, Art (statisch, starre Teile, Skelett), Exportregeln und Verweis auf den Godot-Manifesteintrag des GLB, das die Referenz benutzt |
| `tools/blender/lune_fbx_export.py` | Blender im Hintergrund: Quelle öffnen, Regeln anwenden, prüfen, FBX schreiben, FBX wieder einlesen und erneut prüfen |
| `tools/blender/export_fbx.sh` | Einstieg; macht die FBX-Dateien byteidentisch reproduzierbar |
| `Assets/Luneraria/Art/<Gruppe>/<id>.fbx` und `<id>.export.json` | Ergebnis: Modell und Protokoll (Blender-Fakten und Prüfungen) |
| `Assets/Luneraria/Editor/Art/LuneModelPostprocessor.cs` | Importregeln in Unity |
| `Assets/Luneraria/Editor/Art/LuneAssetVerifier.cs` | Unity-Prüfung jedes importierten Modells gegen das Blender-Protokoll |
| `Assets/Luneraria/Editor/Art/LuneAssetPreview.cs` | Vorschaubilder in URP |
- Maßstab (1 Einheit = 1 m) und Achsen (Godot Y-oben/−Z-vorn gegenüber Unity Y-oben/+Z-vorn). ```bash
- Root Bone und Bone-Namen. sh tools/blender/export_fbx.sh
- Clip-Namen: idle, walk, attack_a, attack_b, hit, ability, death, roar, victory. ```
- Animation Events.
- Materialslots.
- Normalen und Tangenten.
- Blend Shapes, sofern vorhanden.
## Abnahme ```bash
/Applications/Unity/Hub/Editor/6000.3.20f1/Unity.app/Contents/MacOS/Unity -batchmode -projectPath "$PWD" -executeMethod Luneraria.EditorTools.LuneAssetVerifier.VerifyAll -logFile -
```
Weitere Einstiege: `LuneAssetVerifier.ReimportArt` (nach geänderten Importregeln) und `LuneAssetPreview.RenderAll`.
### Exportregeln
Die Regeln wiederholen, was die Godot-Pipeline beim GLB-Export getan hat. Sonst entstünde ein anderes Modell:
- **Phase-4-Einheiten:** Die Modifikatoren „HIGH RES“ (Unterteilung und Mikrorelief) kamen erst *nach* dem Laufzeitexport in die `.blend`-Datei. Sie werden nicht angewendet (bei `g01`: 36 Stück).
- **Logische Namen:** Die Namen stammen aus den Eigenschaften `part_name` und `material_name`; Blender hatte teils Suffixe wie `.004` ergänzt.
- **Figuren mit Skelett:** Die `.blend`-Datei ist vor dem Zusammenfügen gespeichert. Der Export fügt die Teile wie in Godot zu einem Skin-Mesh zusammen (Funkenlanze: 104 Teile → `FunkenBody`).
- **Achsen:** Die FBX-Datei behält Blenders Z-oben-Achsen; Unity rechnet sie beim Import mit „Bake Axis Conversion“ um, also ohne Wurzelrotation. Blender (x, y, z) wird dabei Unity (x, z, y); die Vorderseite (Blender −Y) läge auf Unity −Z.
- Deshalb dreht der Export jedes Asset vorher um 180° um die Hochachse und wendet die Drehung an. Danach gilt Unity = (−x, y, z) der Godot-/glTF-Koordinaten, und die Vorderseite liegt auf +Z wie in Godot.
- Blenders „Apply Transform“ ist für Skelette als fehlerhaft dokumentiert und wird nicht benutzt.
- **Maßstab und Inhalt:** 1 Blender-Einheit = 1 m; Normalen, Tangenten (MikkTSpace, wo UVs existieren), lineare Vertexfarben; nur Deform-Bones, keine Leaf-Bones; jede Action wird ein Clip.
- **Reproduzierbarkeit:** Blenders FBX-Schreiber stempelt die Uhrzeit ein und leitet Objekt-IDs aus Pythons String-Hash ab. Der Export fixiert beides (feste Uhrzeit, `PYTHONHASHSEED=0` mit `--python-use-system-env`). Zwei Läufe liefern byteidentische FBX-Dateien und identische Protokolle.
### Prüfungen
Blender-Seite, vor und nach dem Schreiben (Rundreise durch einen FBX-Import):
| Prüfpunkt | Wie |
| --- | --- |
| Einheiten, Maßstab | metrisch, `scale_length` 1, keine Objektskalierung, keine negative Skalierung |
| Achsen, Vorderseite | Drehung angewendet; Vorwärtsmarker (Phase 4: Empty `Forward`) liegt in Unity auf +Z |
| Mesh-Namen | eindeutig; gleich den Knoten des Godot-GLB |
| Dreiecke | gleich dem Godot-GLB, auch nach dem Wiedereinlesen |
| Materialslots | keine leeren Slots; Materialien gleich dem Godot-GLB |
| Normalen | Einheitslänge an allen Ecken |
| Tangenten | Voraussetzungen (UVs, keine N-Ecke); Meshes ohne UVs sind vermerkt und werden triplanar oder unbeleuchtet schattiert |
| Maße | Ruhelage gleich den `bounds_godot` des Godot-Manifests (Türme und Ancient: ohne Schadens- und Ruinenteile, wie dort gemessen) |
| Skelett | genau eine Armature, ein Root Bone `Root`, Bone-Namen und -Anzahl wie im Godot-GLB, portable Namen |
| Clips | Namen und Längen wie im Godot-GLB; Pflichtclips vorhanden; Loop-Flag und Kontaktzeit aus den Action-Eigenschaften |
| Blend Shapes | keine in den Quellen |
Unity-Seite (`LuneAssetVerifier`, gegen das Blender-Protokoll):
| Prüfpunkt | Wie |
| --- | --- |
| Importregeln | Maßstab 1, Dateieinheiten, Achsen-Bake, Normalen und Tangenten aus der Datei |
| Achsen | Wurzel und alle Nicht-Bone-Knoten ohne Rotation und Skalierung |
| Mesh-Namen, Dreiecke | Menge und Anzahl je Mesh gleich |
| Maße | aus den Vertices berechnet, Abweichung höchstens 2 mm; gemessen höchstens 0,05 mm |
| Materialslots | Namen je Renderer gleich den Blender-Slots |
| Normalen, Tangenten, UVs, Vertexfarben | vorhanden und gültig, wo Blender sie hat; Tangenten mit w = ±1 |
| Vorderseite | Vorwärtsmarker auf +Z |
| Skelett | Root Bone, Bone-Namen, gültiger Generic-Avatar |
| Clips | Namen, Längen (auf einen halben Frame genau), Loop-Flags, Kontakt-Events (`OnLuneContact`) |
### Ergebnis: sechs repräsentative Assets und drei Szenenbauten
| Asset | vertritt | Dreiecke (= Godot-GLB) | Blender-Prüfungen | Unity-Prüfungen |
| --- | --- | ---: | --- | --- |
| `g01` Kohlenbeißer | normale Einheit, 131 benannte Teile | 79.780 | 18/18 | 14/14 |
| `funkenlanze` | Einheit mit Skelett (Held), 26 Bones, 9 Clips | 24.036 | 26/26 | 18/18 |
| `grenzwacht` | Turm (mit Schadens- und Ruinenteilen) | 6.516 | 18/18 | 13/13 |
| `ancient` Mondherz | Ancient | 19.032 | 18/18 | 13/13 |
| `schlundkrone` | Schlundkrone (Boss), 24 Bones, 11 Clips | 5.712 | 28/28 | 18/18 |
| `map_props` | Vegetation und Kartenrequisiten (Eichen, Kiefern, Sträucher, Felsen, Brücke, Randsteine) | 7.338 | 14/14 | 13/13 |
| `bastion`, `kernwacht`, `bossgrotte` | für die Szene in U2B (Innen- und Basisturm, Mondschlund) | 9.364, 9.224, 15.182 | je 18/18 | je 13/13 |
Summe: 176 Blender-Prüfungen und 128 Unity-Prüfungen, alle bestanden. Protokolle:
- `Assets/Luneraria/Art/*/*.export.json` (Blender).
- `docs/port/logs/u2a/unity-verify.json` (Unity).
Bilder unter `docs/port/screenshots/u2a/`:
- je Asset eine Ansicht von vorn-rechts,
- die Grenzwacht zusätzlich als Ruine,
- Funkenlanze und Schlundkrone zusätzlich im Kontaktframe von `attack_a` (Clips laufen).
### Bekannte Grenzen
- **Materialien:** Die Vorschau benutzt URP-Lit-Materialien, die Unity aus den FBX-Materialbeschreibungen erzeugt (Blender-Palettenfarben). Das eigentliche Materialsystem (Stein, Metall, Kristall, Runen, Stoff, Wasser; Teamfarben; Vertex-AO) folgt in U2B.
- Stoff (Wimpel, Banner) ist einseitig modelliert und braucht dort doppelseitiges Rendern; in der Vorschau fehlt er deshalb.
- **Ohne UVs:** Diese Meshes haben keine Tangenten (Unity meldet das beim Import): Kristalle, Trümmer, Felsen, Kartenrequisiten und einige Phase-4-Kleinteile. Sie brauchen triplanare oder unbeleuchtete Materialien.
- **Starre Einheiten:** Die Phase-4-Einheiten werden in Godot aus benannten Teilen prozedural animiert. Die Teile kommen mit, die Animation folgt mit den Einheitenansichten.
- **Größe:** Die FBX-Dateien liegen direkt im Repository (9 Dateien, 16 MB). Für den Batchlauf über alle Assets ist Git LFS zu erwägen.
### Batchlauf (nach dem Vertical Function Test)
Das Manifest enthält bisher die 9 Assets der Szene. Die übrigen Assets kommen erst nach U2D in das Manifest und werden dann mit denselben Regeln und Prüfungen exportiert:
- 58 weitere Phase-4-Modelle.
- 5 Figuren mit Skelett.
- 7 Völker-Modelle. Für Zwerg und Troll fehlt die `.blend`-Datei im Referenz-Repository; sie entsteht aus dem Generator `build_peoples.py`.
Bauten aus einer GLB ohne Quelle folgen nur nach Einzelfallprüfung.
## U2B bis U2D
Folgen in diesem Dokument.
## Abnahme (gesamt)
- Die Partie läuft sichtbar. - Die Partie läuft sichtbar.
- Die Kamera entspricht der Godot-Kamera. - Die Kamera entspricht der Godot-Kamera.
- Screenshots neben den Godot-G2-Bildern mit vergleichbarer Kamera. - Screenshots neben den Godot-G2-Bildern mit vergleichbarer Kamera.
- Leistung gemessen, einschließlich Mono-Simulationszeit. - Leistung gemessen, einschließlich Mono-Simulationszeit.
- Bekannte Unterschiede dokumentiert. - Bekannte Unterschiede dokumentiert.
- Commit auf `luneraria/u2`. - Commits U2A bis U2D auf `luneraria/u2`.

View file

@ -0,0 +1,968 @@
{
"unity": "6000.3.20f1",
"ok": true,
"assets": [
{
"id": "ancient",
"kind": "static",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Buildings/ancient.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"ancient pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"19032 unity / 19032 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00005 m; unity (-4.204, -0.988, -4.000) (4.204, 10.367, 4.000)\""
}
],
"facts": {
"triangles": 19032,
"renderers": 27,
"meshes_without_uv_and_tangents": 14,
"bounds_min": [
-4.20353031,
-0.9881463,
-4.0
],
"bounds_max": [
4.20352936,
10.3668232,
4.0
]
}
},
{
"id": "bastion",
"kind": "static",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Buildings/bastion.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"bastion pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"9364 unity / 9364 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00005 m; unity (-2.116, -0.389, -2.539) (2.901, 7.161, 1.950)\""
}
],
"facts": {
"triangles": 9364,
"renderers": 11,
"meshes_without_uv_and_tangents": 6,
"bounds_min": [
-2.11555481,
-0.3890798,
-2.538877
],
"bounds_max": [
2.90077925,
7.160508,
1.94963574
]
}
},
{
"id": "bossgrotte",
"kind": "static",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Buildings/bossgrotte.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"bossgrotte pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"15182 unity / 15182 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00004 m; unity (-7.777, -1.930, -10.465) (10.544, 7.693, 7.798)\""
}
],
"facts": {
"triangles": 15182,
"renderers": 12,
"meshes_without_uv_and_tangents": 4,
"bounds_min": [
-7.77746248,
-1.93,
-10.4646072
],
"bounds_max": [
10.5441828,
7.69265556,
7.79764366
]
}
},
{
"id": "grenzwacht",
"kind": "static",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Buildings/grenzwacht.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"grenzwacht pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"6516 unity / 6516 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00005 m; unity (-2.258, -0.065, -2.553) (1.966, 6.060, 1.368)\""
}
],
"facts": {
"triangles": 6516,
"renderers": 7,
"meshes_without_uv_and_tangents": 2,
"bounds_min": [
-2.25751829,
-0.06533188,
-2.55256557
],
"bounds_max": [
1.96584618,
6.060136,
1.36833835
]
}
},
{
"id": "kernwacht",
"kind": "static",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Buildings/kernwacht.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"kernwacht pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"9224 unity / 9224 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00004 m; unity (-6.076, -2.396, -2.194) (2.396, 8.330, 1.813)\""
}
],
"facts": {
"triangles": 9224,
"renderers": 9,
"meshes_without_uv_and_tangents": 2,
"bounds_min": [
-6.07622528,
-2.39588761,
-2.19388914
],
"bounds_max": [
2.39623713,
8.33,
1.81258774
]
}
},
{
"id": "funkenlanze",
"kind": "skinned",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Characters/funkenlanze.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"funkenlanze pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"24036 unity / 24036 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00005 m; unity (-0.565, -0.010, -0.295) (0.430, 2.021, 1.435)\""
},
{
"check": "skinned_mesh",
"ok": true,
"detail": "1"
},
{
"check": "bone_names FunkenBody",
"ok": true,
"detail": "[]"
},
{
"check": "root_bone FunkenBody",
"ok": true,
"detail": "\"Root\""
},
{
"check": "generic_avatar",
"ok": true,
"detail": "\"funkenlanzeAvatar\""
},
{
"check": "clips",
"ok": true,
"detail": "[]"
}
],
"facts": {
"triangles": 24036,
"renderers": 1,
"meshes_without_uv_and_tangents": 0,
"bounds_min": [
-0.5651692,
-0.0104093477,
-0.2945487
],
"bounds_max": [
0.43,
2.02119541,
1.43500006
],
"clips": [
{
"name": "ability",
"length": 1.3,
"loop": false,
"events": 1
},
{
"name": "attack_a",
"length": 1.3,
"loop": false,
"events": 1
},
{
"name": "attack_b",
"length": 1.3,
"loop": false,
"events": 1
},
{
"name": "death",
"length": 2.0,
"loop": false,
"events": 0
},
{
"name": "hit",
"length": 0.5,
"loop": false,
"events": 0
},
{
"name": "idle",
"length": 3.0,
"loop": true,
"events": 0
},
{
"name": "start",
"length": 0.4,
"loop": false,
"events": 0
},
{
"name": "stop",
"length": 0.4,
"loop": false,
"events": 0
},
{
"name": "walk",
"length": 1.2,
"loop": true,
"events": 0
}
]
}
},
{
"id": "schlundkrone",
"kind": "skinned",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Characters/schlundkrone.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"schlundkrone pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"5712 unity / 5712 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00005 m; unity (-1.327, 0.005, -0.553) (1.231, 3.318, 0.925)\""
},
{
"check": "skinned_mesh",
"ok": true,
"detail": "1"
},
{
"check": "bone_names SchlundkroneBody",
"ok": true,
"detail": "[]"
},
{
"check": "root_bone SchlundkroneBody",
"ok": true,
"detail": "\"Root\""
},
{
"check": "generic_avatar",
"ok": true,
"detail": "\"schlundkroneAvatar\""
},
{
"check": "clips",
"ok": true,
"detail": "[]"
}
],
"facts": {
"triangles": 5712,
"renderers": 1,
"meshes_without_uv_and_tangents": 0,
"bounds_min": [
-1.32726932,
0.00534708425,
-0.5531612
],
"bounds_max": [
1.23073983,
3.31821656,
0.9253263
],
"clips": [
{
"name": "ability",
"length": 1.3,
"loop": false,
"events": 0
},
{
"name": "attack_a",
"length": 1.3,
"loop": false,
"events": 1
},
{
"name": "attack_b",
"length": 1.3,
"loop": false,
"events": 1
},
{
"name": "death",
"length": 2.2,
"loop": false,
"events": 0
},
{
"name": "hit",
"length": 0.5,
"loop": false,
"events": 0
},
{
"name": "idle",
"length": 3.0,
"loop": true,
"events": 0
},
{
"name": "roar",
"length": 2.0,
"loop": false,
"events": 0
},
{
"name": "start",
"length": 0.4,
"loop": false,
"events": 0
},
{
"name": "stop",
"length": 0.4,
"loop": false,
"events": 0
},
{
"name": "victory",
"length": 2.0,
"loop": false,
"events": 0
},
{
"name": "walk",
"length": 1.6,
"loop": true,
"events": 0
}
]
}
},
{
"id": "map_props",
"kind": "static",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Environment/map_props.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"map_props pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"7338 unity / 7338 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00002 m; unity (-7.368, -1.300, -3.900) (7.455, 5.244, 3.900)\""
}
],
"facts": {
"triangles": 7338,
"renderers": 15,
"meshes_without_uv_and_tangents": 15,
"bounds_min": [
-7.368419,
-1.3,
-3.90000057
],
"bounds_max": [
7.455483,
5.244,
3.90000057
]
}
},
{
"id": "g01",
"kind": "rigid",
"ok": true,
"checks": [
{
"check": "blender_checks_passed",
"ok": true,
"detail": ""
},
{
"check": "imported",
"ok": true,
"detail": "\"Assets/Luneraria/Art/Units/g01.fbx\""
},
{
"check": "import_rules_applied",
"ok": true,
"detail": ""
},
{
"check": "root_transform_identity",
"ok": true,
"detail": "\"g01 pos (0.0000, 0.0000, 0.0000) rot (0.00, 0.00, 0.00) scale (1.0000, 1.0000, 1.0000)\""
},
{
"check": "node_transforms_axis_clean",
"ok": true,
"detail": "[]"
},
{
"check": "mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "triangles",
"ok": true,
"detail": "\"79780 unity / 79780 blender \""
},
{
"check": "material_slots",
"ok": true,
"detail": "[]"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "[]"
},
{
"check": "tangents_where_uv",
"ok": true,
"detail": "[]"
},
{
"check": "uvs_where_blender_has_uv",
"ok": true,
"detail": "[]"
},
{
"check": "vertex_colors",
"ok": true,
"detail": "[]"
},
{
"check": "bounds_axes_scale",
"ok": true,
"detail": "\"largest difference 0.00004 m; unity (-0.792, 0.000, -0.447) (0.792, 1.350, 0.447)\""
},
{
"check": "forward_is_plus_z",
"ok": true,
"detail": "\"(0.000, 0.000, 0.750)\""
}
],
"facts": {
"triangles": 79780,
"renderers": 131,
"meshes_without_uv_and_tangents": 38,
"bounds_min": [
-0.792443752,
0.0,
-0.447301447
],
"bounds_max": [
0.7924438,
1.35,
0.447301567
]
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

View file

@ -0,0 +1,11 @@
#!/bin/sh
# Blender -> FBX export of the Unity port (U2A): sh tools/blender/export_fbx.sh [id,id,...]
# Without ids every asset of tools/blender/export_manifest.json is exported. Needs Blender 5.2
# (BLENDER, default "blender") and the Godot reference (GODOT_PROJECT, default ../saumwacht-godot).
# PYTHONHASHSEED=0 with --python-use-system-env makes the FBX object ids reproducible.
set -eu
cd "$(dirname "$0")/../.."
BLENDER="${BLENDER:-blender}"
GODOT_PROJECT="${GODOT_PROJECT:-../saumwacht-godot}"
PYTHONHASHSEED=0 "$BLENDER" --background --factory-startup --python-use-system-env \
--python tools/blender/lune_fbx_export.py -- --godot "$GODOT_PROJECT" --ids "${1:-}"

View file

@ -0,0 +1,58 @@
{
"comment": "Export rules per asset for tools/blender/lune_fbx_export.py. source: Blender file in the Godot reference (../saumwacht-godot). godot_manifest: the manifest entry of the GLB the reference game uses; the FBX must match it (triangles, names, materials, bounds, bones, clips). kind: static | rigid (named parts, animated by the view) | skinned.",
"assets": [
{
"id": "g01", "group": "Units", "kind": "rigid", "representative": "normal unit (Glutpakt melee, 131 named parts)",
"source": "assets-source/phase4/blend/g01.blend",
"godot_manifest": {"file": "assets/phase4_manifest.json", "id": "g01"},
"rules": {"skip_modifiers": ["HIGH RES"], "logical_names": true, "forward_marker": "Forward", "check_parts": false}
},
{
"id": "funkenlanze", "group": "Characters", "kind": "skinned", "representative": "rigged unit (hero Funkenlanze)",
"source": "assets-source/phase18/funkenlanze.blend",
"godot_manifest": {"file": "assets/phase18_manifest.json", "id": "g05"},
"rules": {"join": {"parent": "FunkenRig", "into": "FunkenBody"}, "rest_action": null, "root_bone": "Root",
"required_clips": ["idle", "walk", "attack_a", "attack_b", "hit", "ability", "death"]}
},
{
"id": "grenzwacht", "group": "Buildings", "kind": "static", "representative": "tower (outer lane tower)",
"source": "assets-source/luneraria/g2/grenzwacht.blend",
"godot_manifest": {"file": "assets/luneraria/g2/g2_manifest.json", "id": "grenzwacht"},
"rules": {"bounds_exclude": ["Upper_Damaged", "Debris", "Ruin", "Pennant", "Banner"]}
},
{
"id": "ancient", "group": "Buildings", "kind": "static", "representative": "Ancient (Mondherz)",
"source": "assets-source/luneraria/g2/ancient.blend",
"godot_manifest": {"file": "assets/luneraria/g2/g2_manifest.json", "id": "ancient"},
"rules": {"bounds_exclude": ["Banner", "Ruin_"]}
},
{
"id": "schlundkrone", "group": "Characters", "kind": "skinned", "representative": "Schlundkrone (boss)",
"source": "assets-source/luneraria/g2/schlundkrone.blend",
"godot_manifest": {"file": "assets/luneraria/g2/g2_manifest.json", "id": "schlundkrone"},
"rules": {"rest_action": null, "root_bone": "Root",
"required_clips": ["idle", "walk", "attack_a", "attack_b", "hit", "ability", "death", "roar", "victory"]}
},
{
"id": "map_props", "group": "Environment", "kind": "static", "representative": "vegetation and map props (oaks, pines, shrubs, rocks, bridge, curb)",
"source": "assets-source/luneraria/map_props.blend"
},
{
"id": "bastion", "group": "Buildings", "kind": "static",
"source": "assets-source/luneraria/g2/bastion.blend",
"godot_manifest": {"file": "assets/luneraria/g2/g2_manifest.json", "id": "bastion"},
"rules": {"bounds_exclude": ["Upper_Damaged", "Debris", "Ruin", "Pennant", "Banner"]}
},
{
"id": "kernwacht", "group": "Buildings", "kind": "static",
"source": "assets-source/luneraria/g2/kernwacht.blend",
"godot_manifest": {"file": "assets/luneraria/g2/g2_manifest.json", "id": "kernwacht"},
"rules": {"bounds_exclude": ["Upper_Damaged", "Debris", "Ruin", "Pennant", "Banner"]}
},
{
"id": "bossgrotte", "group": "Buildings", "kind": "static",
"source": "assets-source/luneraria/g2/bossgrotte.blend",
"godot_manifest": {"file": "assets/luneraria/g2/g2_manifest.json", "id": "bossgrotte"}
}
]
}

View file

@ -0,0 +1,33 @@
"""Prints a JSON summary of a .blend file (objects, meshes, materials, armatures, actions).
Usage: blender --background <file.blend> --python tools/blender/inspect_blend.py -- <out.json>"""
import bpy, json, sys
out = sys.argv[sys.argv.index('--') + 1] if '--' in sys.argv else None
scene = bpy.context.scene
info = {'file': bpy.data.filepath, 'unit_system': scene.unit_settings.system, 'scale_length': scene.unit_settings.scale_length,
'fps': scene.render.fps, 'objects': [], 'actions': []}
for obj in bpy.data.objects:
entry = {'name': obj.name, 'type': obj.type, 'parent': obj.parent.name if obj.parent else None,
'location': [round(v, 4) for v in obj.location], 'rotation': [round(v, 4) for v in obj.rotation_euler],
'scale': [round(v, 4) for v in obj.scale], 'in_scene': obj.name in scene.objects,
'hide_render': obj.hide_render}
if obj.type == 'MESH':
me = obj.data
entry.update({'mesh': me.name, 'verts': len(me.vertices), 'polys': len(me.polygons),
'ngons': sum(1 for p in me.polygons if len(p.vertices) > 4),
'materials': [m.name if m else None for m in me.materials], 'uv_layers': [uv.name for uv in me.uv_layers],
'color_attributes': [(c.name, c.domain, c.data_type) for c in me.color_attributes],
'custom_normals': me.has_custom_normals, 'shape_keys': [k.name for k in me.shape_keys.key_blocks] if me.shape_keys else [],
'modifiers': [(m.type, m.name) for m in obj.modifiers], 'vertex_groups': len(obj.vertex_groups)})
if obj.type == 'ARMATURE':
arm = obj.data
entry.update({'bones': len(arm.bones), 'roots': [b.name for b in arm.bones if b.parent is None],
'bone_names': [b.name for b in arm.bones][:80], 'action': obj.animation_data.action.name if obj.animation_data and obj.animation_data.action else None,
'nla': [(t.name, [s.action.name for s in t.strips if s.action]) for t in obj.animation_data.nla_tracks] if obj.animation_data else []})
info['objects'].append(entry)
for act in bpy.data.actions:
info['actions'].append({'name': act.name, 'range': [round(v, 2) for v in act.frame_range], 'users': act.users, 'fake_user': act.use_fake_user})
text = json.dumps(info, indent=1)
if out:
open(out, 'w').write(text)
else:
print(text)

View file

@ -0,0 +1,454 @@
"""Reproducible Blender -> FBX export for the Unity port (docs/port/U2-WORLD.md, U2A).
Runs headless on the Blender sources of the Godot reference, applies the per-asset export
rules of tools/blender/export_manifest.json, checks the result and writes, per asset:
Assets/Luneraria/Art/<group>/<id>.fbx the model for Unity
Assets/Luneraria/Art/<group>/<id>.export.json facts and checks (read by Unity's importer)
Checks: units and scale, up and forward axis, object transforms, mesh names, triangle counts,
material slots, UVs, normals, tangent prerequisites, vertex colours, root bone, bone names,
clips (names, lengths, loop flags, contact events), bounds. Expectations come from the Godot
manifests of the GLBs the reference game uses, so the FBX must describe the same model.
After writing, the FBX is imported into an empty scene and checked again (round trip).
Usage (from the Unity project root): sh tools/blender/export_fbx.sh [id,id,...]
(runs blender --background --factory-startup --python-use-system-env with PYTHONHASHSEED=0
--python tools/blender/lune_fbx_export.py -- --godot ../saumwacht-godot --ids <ids>)
Exit code 0 only if every check of every asset passed.
"""
import bpy
import hashlib
import json
import math
import sys
from pathlib import Path
from mathutils import Matrix, Vector
UNITY_ROOT = Path(__file__).resolve().parents[2]
EXPORTER_VERSION = 5
# One export setting for every asset. The file keeps Blender's Z-up axes; Unity's importer
# converts them with "Bake Axis Conversion" as Unity (x, y, z) = Blender (x, z, y), so no root
# carries a rotation and animation data is converted with the meshes (Blender's own "Apply
# Transform" is documented as broken for armatures). Before export every asset is turned by 180
# degrees about Blender's Z and the turn is applied (face_unity_forward): the front (Blender -Y,
# glTF/Godot +Z) then becomes Unity +Z, and Unity = (-x, y, z) of the Godot coordinates.
FBX_SETTINGS = dict(
use_selection=True, use_visible=False, use_active_collection=False,
global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_UNITS',
axis_forward='Y', axis_up='Z', bake_space_transform=False,
object_types={'EMPTY', 'MESH', 'ARMATURE'},
use_mesh_modifiers=True, use_mesh_modifiers_render=True,
mesh_smooth_type='OFF', use_subsurf=False, use_mesh_edges=False, use_tspace=True,
use_triangles=False, colors_type='LINEAR', prioritize_active_color=True,
use_custom_props=False,
add_leaf_bones=False, primary_bone_axis='Y', secondary_bone_axis='X',
use_armature_deform_only=True, armature_nodetype='NULL',
bake_anim=True, bake_anim_use_all_bones=True, bake_anim_use_nla_strips=False,
bake_anim_use_all_actions=True, bake_anim_force_startend_keying=True,
bake_anim_step=1.0, bake_anim_simplify_factor=0.0,
path_mode='STRIP', embed_textures=False, batch_mode='OFF',
)
def args():
argv = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else []
result = {'manifest': str(UNITY_ROOT / 'tools/blender/export_manifest.json'), 'godot': str(UNITY_ROOT.parent / 'saumwacht-godot'), 'ids': ''}
for i in range(0, len(argv) - 1, 2):
result[argv[i].lstrip('-')] = argv[i + 1]
return result
class Report:
def __init__(self, spec):
self.data = {'id': spec['id'], 'group': spec['group'], 'source': spec['source'], 'exporter_version': EXPORTER_VERSION,
'blender': bpy.app.version_string, 'fbx_settings': {k: (sorted(v) if isinstance(v, set) else v) for k, v in FBX_SETTINGS.items()},
'checks': []}
def check(self, name, ok, detail=''):
self.data['checks'].append({'check': name, 'ok': bool(ok), 'detail': str(detail)})
return ok
@property
def ok(self):
return all(c['ok'] for c in self.data['checks'])
def godot_expectations(spec, godot_root):
"""Facts of the GLB the reference uses, from its manifest (triangles, nodes, bounds, ...)."""
source = spec.get('godot_manifest')
if not source:
return {}
data = json.loads((godot_root / source['file']).read_text())
assets = data['assets'] if isinstance(data, dict) and 'assets' in data else data
for entry in assets:
if entry.get('id') == source['id']:
return entry
raise KeyError('no entry %s in %s' % (source['id'], source['file']))
def triangles(mesh):
return sum(len(p.vertices) - 2 for p in mesh.polygons)
def evaluated_mesh(obj, depsgraph):
evaluated = obj.evaluated_get(depsgraph)
return evaluated, evaluated.to_mesh()
def prepare(spec, report):
"""Applies the export rules of the manifest to the opened .blend (never saved)."""
rules = spec.get('rules', {})
skip = tuple(rules.get('skip_modifiers', ()))
removed = 0
if skip:
for obj in bpy.data.objects:
for modifier in list(obj.modifiers):
if modifier.name.startswith(skip):
obj.modifiers.remove(modifier)
removed += 1
report.data['removed_modifiers'] = removed
if rules.get('logical_names'):
logical_names(report)
join = rules.get('join')
if join:
parts = [o for o in bpy.context.scene.objects if o.type == 'MESH' and o.parent and o.parent.name == join['parent']]
bpy.ops.object.select_all(action='DESELECT')
for obj in parts:
obj.select_set(True)
bpy.context.view_layer.objects.active = parts[0]
bpy.ops.object.join()
bpy.context.object.name = join['into']
bpy.context.object.data.name = join['into']
report.data['joined_parts'] = len(parts)
face_unity_forward(report)
rest_action = rules.get('rest_action')
for obj in bpy.context.scene.objects:
if obj.type == 'ARMATURE' and obj.animation_data:
# Export the bind pose, not the last evaluated frame of some action.
obj.animation_data.action = bpy.data.actions.get(rest_action) if rest_action else None
bpy.context.scene.frame_set(0)
def face_unity_forward(report):
"""Turns every root 180 degrees about Z and applies the turn to all objects (meshes, empties,
armatures with their rest pose). Bone animation is stored relative to the rest pose and
stays valid. Shared mesh data is made single-user first."""
turn = Matrix.Rotation(math.pi, 4, 'Z')
scene_objects = [o for o in bpy.context.scene.objects if o.type in ('MESH', 'ARMATURE', 'EMPTY')]
for obj in scene_objects:
if obj.parent is None:
obj.matrix_world = turn @ obj.matrix_world
bpy.ops.object.select_all(action='DESELECT')
for obj in scene_objects:
obj.hide_set(False)
obj.hide_select = False
obj.select_set(True)
bpy.context.view_layer.objects.active = scene_objects[0]
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False, isolate_users=True)
turned = [o.name for o in scene_objects if o.matrix_world.to_euler().z != 0.0 and abs(o.matrix_world.to_euler().z) > 1e-5]
report.check('turn_applied', not turned, turned[:8])
def logical_names(report):
"""Phase-4 sources keep their logical names in custom properties (part_name, material_name);
Blender may have suffixed the datablocks (.004). The Godot pipeline renamed the GLB nodes the
same way (assets-source/phase4/blender/pipeline.py, _canonical_glb_names). Materials that share
a logical name are one material and are merged."""
renamed = 0
for obj in list(bpy.data.objects):
if 'part_name' in obj.keys():
obj.name = '__lune_tmp_' + obj.name
for obj in list(bpy.data.objects):
if 'part_name' in obj.keys():
obj.name = obj['part_name']
renamed += int(obj.name == obj['part_name'])
groups = {}
for material in list(bpy.data.materials):
logical = material.get('material_name')
if logical:
groups.setdefault(logical, []).append(material)
merged = 0
for logical, materials in groups.items():
keep = materials[0]
for other in materials[1:]:
other.user_remap(keep)
merged += 1
for material in materials[1:]:
bpy.data.materials.remove(material)
keep.name = '__lune_tmp_' + logical
for logical in groups:
bpy.data.materials['__lune_tmp_' + logical].name = logical
collisions = [o.name for o in bpy.data.objects if 'part_name' in o.keys() and o.name != o['part_name']]
report.check('logical_names_unique', not collisions, collisions[:12])
report.data['logical_names'] = {'objects': renamed, 'materials': len(groups), 'merged_materials': merged}
def export_objects(spec):
include = spec.get('rules', {}).get('objects')
objects = []
for obj in bpy.context.scene.objects:
if obj.type not in ('MESH', 'ARMATURE', 'EMPTY'):
continue
if include and obj.name not in include:
continue
if obj.hide_render:
continue
objects.append(obj)
return objects
def to_unity(v):
"""Blender (x, y, z), after the turn of face_unity_forward -> Unity (x, y, z) = (x, z, y)."""
return Vector((v.x, v.z, v.y))
def facts(objects, depsgraph):
"""Scene facts in Unity coordinates: meshes, materials, bounds, bones, actions."""
meshes = {}
low = Vector((math.inf,) * 3)
high = Vector((-math.inf,) * 3)
for obj in objects:
if obj.type != 'MESH':
continue
evaluated, mesh = evaluated_mesh(obj, depsgraph)
world = evaluated.matrix_world
# Skinned meshes: bounds of the rest pose (armature modifier is evaluated in the bind pose).
mesh_low = Vector((math.inf,) * 3)
mesh_high = Vector((-math.inf,) * 3)
for vertex in mesh.vertices:
p = to_unity(world @ vertex.co)
mesh_low = Vector(map(min, mesh_low, p))
mesh_high = Vector(map(max, mesh_high, p))
low = Vector(map(min, low, mesh_low))
high = Vector(map(max, high, mesh_high))
normals_ok = all(abs(n.vector.length - 1.0) < 1e-3 for n in mesh.corner_normals)
meshes[obj.name] = {
'mesh': obj.data.name, 'triangles': triangles(mesh), 'vertices': len(mesh.vertices),
'ngons': sum(1 for p in mesh.polygons if len(p.vertices) > 4),
'materials': [slot.material.name if slot.material else None for slot in obj.material_slots],
'uv_layers': [uv.name for uv in mesh.uv_layers],
'color_attributes': [c.name for c in mesh.color_attributes],
'active_color': mesh.color_attributes.active_color.name if mesh.color_attributes.active_color else None,
'custom_normals': obj.data.has_custom_normals, 'normals_unit_length': normals_ok,
'shape_keys': len(obj.data.shape_keys.key_blocks) - 1 if obj.data.shape_keys else 0,
'skinned': any(m.type == 'ARMATURE' for m in obj.modifiers),
'scale': [round(s, 6) for s in obj.matrix_world.to_scale()],
'bounds_unity': {'min': [round(c, 4) for c in mesh_low], 'max': [round(c, 4) for c in mesh_high]},
}
evaluated.to_mesh_clear()
armatures = {}
for obj in objects:
if obj.type == 'ARMATURE':
bones = [b for b in obj.data.bones if b.use_deform]
armatures[obj.name] = {'bones': [b.name for b in bones], 'roots': [b.name for b in bones if b.parent is None or not b.parent.use_deform]}
empties = {o.name: [round(c, 4) for c in to_unity(o.matrix_world.translation)] for o in objects if o.type == 'EMPTY'}
return {'meshes': meshes, 'armatures': armatures, 'empties': empties,
'bounds_unity': {'min': [round(c, 4) for c in low], 'max': [round(c, 4) for c in high]} if meshes else None}
def clip_facts(fps):
clips = []
for action in bpy.data.actions:
start, end = action.frame_range
clips.append({'name': action.name, 'frames': [round(start, 3), round(end, 3)], 'seconds': round((end - start) / fps, 4),
'loop': bool(action.get('loop', action.name in ('idle', 'walk'))),
'contact_seconds': float(action.get('contact_seconds', -1.0))})
return clips
def godot_to_unity_bounds(bounds):
"""Godot/glTF bounds (x, y, z) -> Unity (-x, y, z)."""
lo, hi = bounds['min'], bounds['max']
return {'min': [-hi[0], lo[1], lo[2]], 'max': [-lo[0], hi[1], hi[2]]}
def run_checks(spec, report, before, clips, expect):
scene = bpy.context.scene
report.check('units_metric', scene.unit_settings.system == 'METRIC', scene.unit_settings.system)
report.check('unit_scale_1', abs(scene.unit_settings.scale_length - 1.0) < 1e-9, scene.unit_settings.scale_length)
meshes = before['meshes']
bad_scale = {n: m['scale'] for n, m in meshes.items() if any(abs(s - 1.0) > 1e-4 for s in m['scale'])}
negative = {n: s for n, s in bad_scale.items() if any(c < 0 for c in s)}
report.check('no_negative_scale', not negative, negative)
report.check('object_scale_1', not bad_scale or spec.get('rules', {}).get('allow_object_scale', False), bad_scale)
names = [m['mesh'] for m in meshes.values()]
report.check('mesh_names_unique', len(set(meshes)) == len(meshes), sorted(meshes))
empty_slots = {n: m['materials'] for n, m in meshes.items() if None in m['materials'] or not m['materials']}
report.check('material_slots_filled', not empty_slots, empty_slots)
report.check('normals_unit_length', all(m['normals_unit_length'] for m in meshes.values()),
[n for n, m in meshes.items() if not m['normals_unit_length']])
# Tangents need UVs and faces with at most four corners; meshes without UVs are shaded
# triplanar or unlit and carry no tangents (recorded, not an error).
tangent_blockers = {n: m['ngons'] for n, m in meshes.items() if m['uv_layers'] and m['ngons']}
report.check('tangent_prerequisites', not tangent_blockers, tangent_blockers)
report.data['meshes_without_uv'] = sorted(n for n, m in meshes.items() if not m['uv_layers'])
# Blend shapes: none of the sources uses them; Unity imports them only when they exist.
shapes = {n: m['shape_keys'] for n, m in meshes.items() if m['shape_keys']}
report.check('no_blend_shapes', not shapes or spec.get('rules', {}).get('blend_shapes', False), shapes)
total = sum(m['triangles'] for m in meshes.values())
report.data['triangles'] = total
if 'triangles' in expect:
report.check('triangles_match_godot', total == expect['triangles'], '%d fbx / %d godot' % (total, expect['triangles']))
if 'nodes' in expect:
report.check('mesh_names_match_godot', sorted(meshes) == sorted(expect['nodes']), sorted(set(meshes) ^ set(expect['nodes'])))
if 'parts' in expect and spec.get('rules', {}).get('check_parts', True):
report.check('mesh_names_match_godot', sorted(meshes) == sorted(expect['parts']), sorted(set(meshes) ^ set(expect['parts']))[:12])
if 'materials' in expect:
used = sorted({name for m in meshes.values() for name in m['materials'] if name})
report.check('materials_match_godot', used == sorted(expect['materials']), sorted(set(used) ^ set(expect['materials'])))
if expect.get('bounds_godot') and before['bounds_unity']:
# The Godot manifests measure the intact model (towers: without damaged upper part,
# debris, ruin and cloth); bounds_exclude names those parts.
exclude = tuple(spec.get('rules', {}).get('bounds_exclude', ()))
chosen = [m['bounds_unity'] for n, m in meshes.items() if not (exclude and n.startswith(exclude))]
got = {'min': [min(b['min'][i] for b in chosen) for i in range(3)], 'max': [max(b['max'][i] for b in chosen) for i in range(3)]}
want = godot_to_unity_bounds(expect['bounds_godot'])
worst = max(abs(a - b) for key in ('min', 'max') for a, b in zip(got[key], want[key]))
report.check('bounds_match_godot', worst < 2e-3, 'largest difference %.5f m' % worst)
armatures = before['armatures']
if spec['kind'] == 'skinned':
report.check('one_armature', len(armatures) == 1, sorted(armatures))
for name, arm in armatures.items():
report.check('single_root_bone', len(arm['roots']) == 1, arm['roots'])
want_root = spec.get('rules', {}).get('root_bone', 'Root')
report.check('root_bone_name', arm['roots'] == [want_root], arm['roots'])
if 'bone_names' in expect:
report.check('bone_names_match_godot', sorted(arm['bones']) == sorted(expect['bone_names']), sorted(set(arm['bones']) ^ set(expect['bone_names'])))
if 'bones' in expect:
report.check('bone_count_match_godot', len(arm['bones']) == expect['bones'], '%d / %d' % (len(arm['bones']), expect['bones']))
bad = [b for b in arm['bones'] if not b.replace('.', '').replace('_', '').isalnum()]
report.check('bone_names_portable', not bad, bad)
report.check('skinned_meshes_present', any(m['skinned'] for m in meshes.values()), '')
names = sorted(c['name'] for c in clips)
if 'clips' in expect:
report.check('clips_match_godot', names == sorted(expect['clips']), sorted(set(names) ^ set(expect['clips'])))
required = spec.get('rules', {}).get('required_clips', [])
report.check('required_clips_present', all(c in names for c in required), [c for c in required if c not in names])
if 'clip_seconds' in expect:
worst = max(abs(c['seconds'] - expect['clip_seconds'][c['name']]) for c in clips if c['name'] in expect['clip_seconds'])
report.check('clip_lengths_match_godot', worst < 1e-3, 'largest difference %.4f s' % worst)
else:
report.check('no_armature', not armatures, sorted(armatures))
forward = spec.get('rules', {}).get('forward_marker')
if forward:
marker = next((p for n, p in before['empties'].items() if n.startswith(forward)), None)
report.check('forward_marker_unity_plus_z', marker is not None and marker[2] > 0.1 and abs(marker[0]) < 1e-3, marker)
def reproducible_fbx():
"""Byte-identical FBX for identical sources: Blender's FBX writer stamps the current time into
the header and derives object ids from Python's string hash. The clock is fixed here; the hash
seed comes from PYTHONHASHSEED=0 with --python-use-system-env (tools/blender/export_fbx.sh)."""
import datetime
import io_scene_fbx.export_fbx_bin as writer
class FixedClock(datetime.datetime):
@classmethod
def now(cls, tz=None):
return cls(2026, 1, 1, 0, 0, 0)
class Clock:
datetime = FixedClock
writer.datetime = Clock
def export_fbx(path, objects):
bpy.ops.object.select_all(action='DESELECT')
for obj in objects:
obj.select_set(True)
bpy.context.view_layer.objects.active = objects[0]
path.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.export_scene.fbx(filepath=str(path), **FBX_SETTINGS)
def round_trip(path, spec, report, before, clips):
"""Imports the written FBX into an empty file and compares names and counts."""
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=str(path), use_custom_normals=True, use_anim=True, automatic_bone_orientation=False)
depsgraph = bpy.context.evaluated_depsgraph_get()
imported = [o for o in bpy.context.scene.objects]
meshes = {o.name: o for o in imported if o.type == 'MESH'}
report.check('roundtrip_mesh_names', sorted(meshes) == sorted(before['meshes']), sorted(set(meshes) ^ set(before['meshes'])))
tri = 0
for obj in meshes.values():
evaluated, mesh = evaluated_mesh(obj, depsgraph)
tri += triangles(mesh)
evaluated.to_mesh_clear()
report.check('roundtrip_triangles', tri == report.data['triangles'], '%d / %d' % (tri, report.data['triangles']))
materials = sorted({s.material.name for o in meshes.values() for s in o.material_slots if s.material})
want = sorted({name for m in before['meshes'].values() for name in m['materials'] if name})
report.check('roundtrip_materials', materials == want, sorted(set(materials) ^ set(want)))
if spec['kind'] == 'skinned':
arm = next(o for o in imported if o.type == 'ARMATURE')
bones = sorted(b.name for b in arm.data.bones)
want_bones = sorted(next(iter(before['armatures'].values()))['bones'])
report.check('roundtrip_bones', bones == want_bones, sorted(set(bones) ^ set(want_bones)))
takes = sorted(a.name.split('|')[-1] for a in bpy.data.actions)
report.check('roundtrip_clips', takes == sorted(c['name'] for c in clips), takes)
def export_asset(spec, godot_root):
report = Report(spec)
source = godot_root / spec['source']
bpy.ops.wm.open_mainfile(filepath=str(source))
report.data['source_sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
expect = godot_expectations(spec, godot_root)
fps = bpy.context.scene.render.fps / bpy.context.scene.render.fps_base
prepare(spec, report)
objects = export_objects(spec)
# Facts of the bind pose (rest), like the Godot manifests; the animation bake needs POSE again.
armatures = [o for o in objects if o.type == 'ARMATURE']
for arm in armatures:
arm.data.pose_position = 'REST'
bpy.context.view_layer.update()
depsgraph = bpy.context.evaluated_depsgraph_get()
before = facts(objects, depsgraph)
for arm in armatures:
arm.data.pose_position = 'POSE'
bpy.context.view_layer.update()
clips = clip_facts(fps) if spec['kind'] == 'skinned' else []
report.data.update({'kind': spec['kind'], 'fps': fps, 'objects': len(objects), 'meshes': before['meshes'], 'armatures': before['armatures'],
'empties': before['empties'], 'bounds_unity': before['bounds_unity'], 'clips': clips,
'expected_from_godot': {k: expect[k] for k in ('triangles', 'bounds_godot', 'bones', 'clips', 'resource_path') if k in expect}})
run_checks(spec, report, before, clips, expect)
out = UNITY_ROOT / 'Assets/Luneraria/Art' / spec['group'] / (spec['id'] + '.fbx')
export_fbx(out, objects)
report.data['fbx'] = str(out.relative_to(UNITY_ROOT))
report.data['fbx_bytes'] = out.stat().st_size
round_trip(out, spec, report, before, clips)
report.data['ok'] = report.ok
(out.with_suffix('.export.json')).write_text(json.dumps(report.data, indent=1) + '\n')
failed = [c for c in report.data['checks'] if not c['ok']]
print('LUNE_EXPORT %s %s: %d checks, %d failed%s' % ('OK' if not failed else 'FAIL', spec['id'], len(report.data['checks']), len(failed),
''.join('\n FAIL %s: %s' % (c['check'], c['detail']) for c in failed)))
return report.ok
def main():
reproducible_fbx()
options = args()
manifest = json.loads(Path(options['manifest']).read_text())
godot_root = Path(options['godot']).resolve()
wanted = [i for i in options['ids'].split(',') if i]
specs = [s for s in manifest['assets'] if not wanted or s['id'] in wanted]
missing = set(wanted) - {s['id'] for s in specs}
ok = not missing
if missing:
print('LUNE_EXPORT FAIL unknown ids', sorted(missing))
for spec in specs:
try:
ok = export_asset(spec, godot_root) and ok
except Exception as error: # a broken asset must not stop the batch
import traceback
traceback.print_exc()
print('LUNE_EXPORT FAIL %s: %s' % (spec['id'], error))
ok = False
print('LUNE_EXPORT_DONE ok=%s assets=%d' % (ok, len(specs)))
sys.exit(0 if ok else 1)
main()