Compare commits
8 Commits
19c5528f8e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fbe9d1255 | |||
| b8af541c39 | |||
| ea281bf913 | |||
| 214b05b40f | |||
| 141bfd31c1 | |||
| 7e0a74d837 | |||
| 65066e6d29 | |||
| a3ec423cca |
@@ -131,8 +131,26 @@ cmake --build build --target demoLuaSceneScript # Demo: game-mode inline scene/p
|
||||
cmake --build build --target demoSceneSwitchingExtra # Demo: scene switching with CellGrid interior
|
||||
cmake --build build --target demoSokoban # Demo: forklift sokoban activity (vehicle + crates + quest HUD)
|
||||
cmake --build build --target demoTaxi # Demo: taxi driver (procedural town, sedans, NPC traffic, fare chain)
|
||||
cmake --build build --target characters-all # All adult/baby/child/teenager/elder character assets
|
||||
cmake --build build --target character-assets-check # Validate age catalogs/LODs/resource tree
|
||||
# Optional: seed a detailed-nude source blend from the existing base parts
|
||||
cmake --build build --target init-nude-male
|
||||
```
|
||||
|
||||
### Parallel asset builds
|
||||
|
||||
Blender 3.6 batch jobs are not safe to run concurrently (shared user
|
||||
config/addon state and shared build files). Every Blender invocation in
|
||||
the CMake pipeline goes through `${BLENDER_LOCKED}`, which is
|
||||
`flock -x ${BLENDER_LOCK} <blender>`, so `cmake --build build -jN` is
|
||||
safe: only one Blender job runs at a time while CMake copies, compiles
|
||||
and validates in parallel. `flock` must be installed (configure fails
|
||||
otherwise). The default lock is per build tree
|
||||
(`<build-dir>/blender-assets.lock`); if two build trees are built at the
|
||||
same time, configure them with the same lock file, for example
|
||||
`-DBLENDER_LOCK=/tmp/world2-blender-assets.lock`. New custom commands
|
||||
must use `${BLENDER_LOCKED}`, never `${BLENDER}` directly.
|
||||
|
||||
### Build Outputs
|
||||
- `build/Game` - Main game executable (legacy)
|
||||
- `build/Editor` - World editor executable (legacy)
|
||||
@@ -375,6 +393,16 @@ JPH::BodyID body = physics->createBody(shape, 1.0f, position, rotation,
|
||||
`<name>.vehicle.json` scene entity (contract:
|
||||
`assets/blender/vehicles/CAR_MODELING.md`)
|
||||
- `import_vrm.py` - VRM character import
|
||||
- `characters/generate_age_blends.py` + `characters/agegen/` - one-shot
|
||||
adult → age-band seed converter (rig scaling, mesh/shape-key
|
||||
deformation, garment/hair conversion, animation retargeting; contract:
|
||||
`assets/blender/characters/AGES-PLAN.md`)
|
||||
- `characters/make_nude_blend.py` - seeds the optional
|
||||
`nude-<stem>.blend` detailed-nude source files from the existing
|
||||
layer-0 body parts (`init-nude-<stem>` targets; the build appends the
|
||||
file after all clothing stages)
|
||||
- `characters/validate_age_assets.py` - structural validation of the
|
||||
generated age catalogs/LODs/resource tree
|
||||
3. **Build**: CMake processes assets during build
|
||||
4. **Runtime**: Assets loaded from `resources.cfg` paths
|
||||
|
||||
|
||||
+16
-3
@@ -4,6 +4,19 @@ project(world2)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
enable_testing()
|
||||
set(BLENDER "${CMAKE_SOURCE_DIR}/../../blender-bin/bin/blender" CACHE STRING "Blender path")
|
||||
# Blender 3.6 batch jobs are not safe to run concurrently: they share
|
||||
# user config/addon state and the asset scripts read/write the same
|
||||
# build-tree files. Every Blender invocation must go through
|
||||
# ${BLENDER_LOCKED} so only one runs at a time. CMake can still run
|
||||
# copies, converters, Python validation and C++ compilation in parallel.
|
||||
find_program(FLOCK_EXECUTABLE flock)
|
||||
if(NOT FLOCK_EXECUTABLE)
|
||||
message(FATAL_ERROR
|
||||
"flock (util-linux) is required to serialize Blender asset jobs")
|
||||
endif()
|
||||
set(BLENDER_LOCK "${CMAKE_BINARY_DIR}/blender-assets.lock" CACHE STRING
|
||||
"Advisory lock for Blender jobs; share across concurrently-built trees")
|
||||
set(BLENDER_LOCKED "${FLOCK_EXECUTABLE}" -x "${BLENDER_LOCK}" "${BLENDER}")
|
||||
set(CREATE_DIRECTORIES
|
||||
${CMAKE_BINARY_DIR}/assets/blender/shapes/male
|
||||
${CMAKE_BINARY_DIR}/assets/blender/shapes/female
|
||||
@@ -128,7 +141,7 @@ foreach(BUILDING_FILE ${BUILDINGS_SRC})
|
||||
add_custom_command(
|
||||
OUTPUT ${BUILDING_OUTPUT_FILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/resources/buildings
|
||||
COMMAND ${BLENDER} ${BUILDING_FILE}
|
||||
COMMAND ${BLENDER_LOCKED} ${BUILDING_FILE}
|
||||
-b -Y -P
|
||||
${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_buildings.py
|
||||
-- ${BUILDING_OUTPUT_FILE}
|
||||
@@ -148,7 +161,7 @@ foreach(CHARACTER_SHAPE_FILE ${CHARACTER_SHAPES_SRC})
|
||||
OUTPUT ${CHARACTER_SHAPE_OUTPUT_FILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/assets/blender
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/characters/shapes/male/${FILE_NAME}
|
||||
COMMAND ${BLENDER} ${CMAKE_SOURCE_DIR}/assets/blender/${CHARACTER_SHAPE_FILE}
|
||||
COMMAND ${BLENDER_LOCKED} ${CMAKE_SOURCE_DIR}/assets/blender/${CHARACTER_SHAPE_FILE}
|
||||
-b -Y -P
|
||||
${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_characters_ogre.py
|
||||
-- ${CHARACTER_SHAPE_OUTPUT_FILE}
|
||||
@@ -161,7 +174,7 @@ add_custom_target(import_character_shapes ALL DEPENDS ${CHARACTER_SHAPES_OUTPUT_
|
||||
set(WATER_STUFF)
|
||||
add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/water/sea.glb
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/water
|
||||
COMMAND ${BLENDER} ${CMAKE_SOURCE_DIR}/assets/blender/sea.blend
|
||||
COMMAND ${BLENDER_LOCKED} ${CMAKE_SOURCE_DIR}/assets/blender/sea.blend
|
||||
-b -Y -P
|
||||
${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_buildings.py
|
||||
-- ${CMAKE_BINARY_DIR}/water/sea.glb
|
||||
|
||||
@@ -9,7 +9,7 @@ foreach(PARTS_FILE ${PARTS_FILES})
|
||||
add_custom_command(
|
||||
OUTPUT ${PARTS_OUTPUT_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${PARTS_OUTPUT_DIR}
|
||||
COMMAND ${BLENDER} ${CMAKE_CURRENT_SOURCE_DIR}/${PARTS_FILE}
|
||||
COMMAND ${BLENDER_LOCKED} ${CMAKE_CURRENT_SOURCE_DIR}/${PARTS_FILE}
|
||||
-b -Y -P
|
||||
${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_building_parts.py
|
||||
-- ${PARTS_OUTPUT_DIR}
|
||||
@@ -31,7 +31,7 @@ foreach(FURNITURE_FILE ${FURNITURE_FILES})
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/${FURNITURE_FILE}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${FURNITURE_FILE}
|
||||
COMMAND ${BLENDER} -b ${CMAKE_CURRENT_BINARY_DIR}/${FURNITURE_FILE}
|
||||
COMMAND ${BLENDER_LOCKED} -b ${CMAKE_CURRENT_BINARY_DIR}/${FURNITURE_FILE}
|
||||
-Y -P ${CMAKE_CURRENT_SOURCE_DIR}/bake_furniture.py
|
||||
-- ${ATLAS_PART} ${FURNITURE_INDEX} ${FURNITURE_COUNT}
|
||||
${BAKE_SUCCESS}
|
||||
@@ -53,7 +53,7 @@ add_custom_command(
|
||||
OUTPUT ${FURNITURE_ATLAS_PNG} ${FURNITURE_ATLAS_MATERIAL}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -f
|
||||
${FURNITURE_ATLAS_PNG} ${FURNITURE_ATLAS_MATERIAL}
|
||||
COMMAND ${BLENDER} --factory-startup -b -P
|
||||
COMMAND ${BLENDER_LOCKED} --factory-startup -b -P
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/merge_furniture_atlases.py --
|
||||
${FURNITURE_ATLAS_PNG} ${FURNITURE_ATLAS_MATERIAL}
|
||||
${FURNITURE_ATLAS_PARTS}
|
||||
@@ -75,7 +75,7 @@ foreach(FURNITURE_FILE ${FURNITURE_FILES})
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf ${PARTS_OUTPUT_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -f ${EXPORT_SUCCESS}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${PARTS_OUTPUT_DIR}
|
||||
COMMAND ${BLENDER} -b ${BAKED_BLEND}
|
||||
COMMAND ${BLENDER_LOCKED} -b ${BAKED_BLEND}
|
||||
-Y -P
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/export_furniture_parts.py
|
||||
-- ${PARTS_OUTPUT_DIR} ${EXPORT_SUCCESS}
|
||||
@@ -117,12 +117,12 @@ set(FURNITURE_GALLERY_DIR
|
||||
${CMAKE_BINARY_DIR}/resources/buildings/parts/gallery)
|
||||
add_custom_target(furniture_gallery
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${FURNITURE_GALLERY_DIR}
|
||||
COMMAND ${BLENDER} -b
|
||||
COMMAND ${BLENDER_LOCKED} -b
|
||||
${CMAKE_CURRENT_BINARY_DIR}/furniture_baked.blend -Y -P
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/render_furniture_gallery.py --
|
||||
${FURNITURE_GALLERY_DIR}
|
||||
${CMAKE_BINARY_DIR}/resources/buildings/parts/furniture
|
||||
COMMAND ${BLENDER} -b
|
||||
COMMAND ${BLENDER_LOCKED} -b
|
||||
${CMAKE_CURRENT_BINARY_DIR}/furniture-sofa_baked.blend -Y -P
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/render_furniture_gallery.py --
|
||||
${FURNITURE_GALLERY_DIR}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"stems": {
|
||||
"baby-female": {
|
||||
"age": "baby",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-female.blend",
|
||||
"animationSourceHash": "6b258336b456f84f42536bef39e1902bca50cc2cb45aa7dbaa0550c83365f92d",
|
||||
"outputHashes": {
|
||||
"clothes-baby-female-bottom.blend": "25b6b8b5230a5e9d91e76b733ee96856ecea873a9bb118ca5905f8d21c6fe16f",
|
||||
"clothes-baby-female-feet.blend": "4477d51b38b4383849bc3871c2ce83056f01874b7f951e89f082c0a287b457d9",
|
||||
"clothes-baby-female-hair.blend": "77397a68bb1d1aef4d92a1a218390cc8ad1e453bf82fc7e3ba2f1d0d0adc6dc2",
|
||||
"clothes-baby-female-top.blend": "a47bc879db6791056b4646b4428d2017d531b4368b4c245898310e2997c717e7",
|
||||
"edited-normal-baby-female.blend": "0afd20804330a1179a7447c7d4f0bbeed90d5c669bfe82c771a1f6bc737e3dd7"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-baby-female.md",
|
||||
"reportJson": "conversion-report-baby-female.json",
|
||||
"sex": "female",
|
||||
"sourceHashes": {
|
||||
"clothes-female-bottom.blend": "c7a5cff31917b176c53aab88d9b22b816ad684ff906d1af856e146a4e3167beb",
|
||||
"clothes-female-feet.blend": "f07ea8aa5145137297b3b556a63e93ba8effa87fb123abb99754925553e0ace1",
|
||||
"clothes-female-hair.blend": "d0aa11db762f9f6974949e4cc579f66c211d7fc913def2c77b71a0ad9199f7bc",
|
||||
"clothes-female-top.blend": "0115190c186d740ac54527dbba944e7b2f0f448f4fa6bff89f3aaba2a02d1439",
|
||||
"edited-normal-female.blend": "b62d4160656b57752cb97fc81c517007c689ad93c510ef2ebc869ac410d13b51"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419320
|
||||
},
|
||||
"baby-male": {
|
||||
"age": "baby",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-male.blend",
|
||||
"animationSourceHash": "a495ad4c4f73bf9971c7f180facbca29d117e002b258380ac946274c5e7b9fee",
|
||||
"outputHashes": {
|
||||
"clothes-baby-male-bottom.blend": "648803be203afe6e1a01553c8f9373a1e136881eb8a0edd1bfe2f837e9bced1c",
|
||||
"clothes-baby-male-feet.blend": "8d3eb2c8c9053c953895508f2de03b6e3abbd23ceb53ebb2da2025b0be3a0a97",
|
||||
"clothes-baby-male-hair.blend": "3e74c20b81b9cb19b4d79a862a5793e7d06726ee9ea8de6d0daeb269b3d4bd94",
|
||||
"clothes-baby-male-top.blend": "124fd1ad4f22f9efc1c3061914a8a74ea1e328af76d064d9080dc171bf3e0ed3",
|
||||
"edited-normal-baby-male.blend": "68537022ec4087743d636336b00bf33be3ed2028d0006e4b0ce2847350ba8737"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-baby-male.md",
|
||||
"reportJson": "conversion-report-baby-male.json",
|
||||
"sex": "male",
|
||||
"sourceHashes": {
|
||||
"clothes-male-bottom.blend": "950298780654bd908c15045d8afe572de755ddb1aa38e548c1dfb912b1cc059c",
|
||||
"clothes-male-feet.blend": "f720ce5cd154ae967673dbc2c6a93a25640dad56760586d205a18ec7b550e39c",
|
||||
"clothes-male-hair.blend": "2b267783d2f0dc991c7edd972b6c21b344abf3b5a46737c664912feae0ca9549",
|
||||
"clothes-male-top.blend": "4ff80187a33acc2a4f6228dd6492b84aee342b22a18c68d2f4c9be9fb15ec9cf",
|
||||
"edited-normal-male.blend": "4cba5e5da1e22eeedd6962d24edb1ef6ecf56c9b719c2f623ee3983c235560ff"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419311
|
||||
},
|
||||
"child-female": {
|
||||
"age": "child",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-female.blend",
|
||||
"animationSourceHash": "6b258336b456f84f42536bef39e1902bca50cc2cb45aa7dbaa0550c83365f92d",
|
||||
"outputHashes": {
|
||||
"clothes-child-female-bottom.blend": "924f20a2edbb56e8e41376fcd8ad58e6c3533707fc8cf7750451ed35e811d853",
|
||||
"clothes-child-female-feet.blend": "900a21c055e9464030e0248941099acb6838dce72c9b4bef72df679427589167",
|
||||
"clothes-child-female-hair.blend": "3f1766cdcf6102fafe030b99daa2ae5a78023bc9c2f6146d8a4924faa2a3cd57",
|
||||
"clothes-child-female-top.blend": "3950729a7079297aff7aa2d4be255813eaf69348dee4688a61c470a2493a6f05",
|
||||
"edited-normal-child-female.blend": "f8f22f31a68b40088fefc1a037b4a9622aafad2b952d07f1ab85d45876bf9882"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-child-female.md",
|
||||
"reportJson": "conversion-report-child-female.json",
|
||||
"sex": "female",
|
||||
"sourceHashes": {
|
||||
"clothes-female-bottom.blend": "c7a5cff31917b176c53aab88d9b22b816ad684ff906d1af856e146a4e3167beb",
|
||||
"clothes-female-feet.blend": "f07ea8aa5145137297b3b556a63e93ba8effa87fb123abb99754925553e0ace1",
|
||||
"clothes-female-hair.blend": "d0aa11db762f9f6974949e4cc579f66c211d7fc913def2c77b71a0ad9199f7bc",
|
||||
"clothes-female-top.blend": "0115190c186d740ac54527dbba944e7b2f0f448f4fa6bff89f3aaba2a02d1439",
|
||||
"edited-normal-female.blend": "b62d4160656b57752cb97fc81c517007c689ad93c510ef2ebc869ac410d13b51"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419306
|
||||
},
|
||||
"child-male": {
|
||||
"age": "child",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-male.blend",
|
||||
"animationSourceHash": "a495ad4c4f73bf9971c7f180facbca29d117e002b258380ac946274c5e7b9fee",
|
||||
"outputHashes": {
|
||||
"clothes-child-male-bottom.blend": "631f0ae0796b29797716be04c0cc8940693a40f673a97d5f6fd219ea541302c3",
|
||||
"clothes-child-male-feet.blend": "2bed6d24379ec2b322884324d8faea6cd0b1ddc6f62f4bbc4f6396f7adff623b",
|
||||
"clothes-child-male-hair.blend": "6e02f2bbf3f06365d6cde9acbe178e869b417dcb1b163db0dad9e2999395e6bc",
|
||||
"clothes-child-male-top.blend": "8aef24cab77cb209af0393b5bf059e61d411d77236c27bbbd0b126bf63c9211c",
|
||||
"edited-normal-child-male.blend": "6d889491af59a9551f4237cd50fba43bdb1b0aeffbf2e6cc0d5e572da9ea6c53"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-child-male.md",
|
||||
"reportJson": "conversion-report-child-male.json",
|
||||
"sex": "male",
|
||||
"sourceHashes": {
|
||||
"clothes-male-bottom.blend": "950298780654bd908c15045d8afe572de755ddb1aa38e548c1dfb912b1cc059c",
|
||||
"clothes-male-feet.blend": "f720ce5cd154ae967673dbc2c6a93a25640dad56760586d205a18ec7b550e39c",
|
||||
"clothes-male-hair.blend": "2b267783d2f0dc991c7edd972b6c21b344abf3b5a46737c664912feae0ca9549",
|
||||
"clothes-male-top.blend": "4ff80187a33acc2a4f6228dd6492b84aee342b22a18c68d2f4c9be9fb15ec9cf",
|
||||
"edited-normal-male.blend": "4cba5e5da1e22eeedd6962d24edb1ef6ecf56c9b719c2f623ee3983c235560ff"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419297
|
||||
},
|
||||
"elder-female": {
|
||||
"age": "elder",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-female.blend",
|
||||
"animationSourceHash": "6b258336b456f84f42536bef39e1902bca50cc2cb45aa7dbaa0550c83365f92d",
|
||||
"outputHashes": {
|
||||
"clothes-elder-female-bottom.blend": "8b9c1be20d83b46ee33397283625e03b2e147c3e35eea5b9eb3b47696f4bea66",
|
||||
"clothes-elder-female-feet.blend": "69c1e995860e5678560cfe76e1349828e49d230d7ee3b4167767bb6708325da1",
|
||||
"clothes-elder-female-hair.blend": "fced301a4709adf9e8a05a17583b8879b0a9f154bd532ed567df9ce924f2859b",
|
||||
"clothes-elder-female-top.blend": "f783777345dfeb83811adc349a4b4f7c21bd5f3085f13c2b218637298f184bad",
|
||||
"edited-normal-elder-female.blend": "570ff96ebfa9005a3c21755603b9a4942ab8336461c7af74bdd970ca1871e9c2"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-elder-female.md",
|
||||
"reportJson": "conversion-report-elder-female.json",
|
||||
"sex": "female",
|
||||
"sourceHashes": {
|
||||
"clothes-female-bottom.blend": "c7a5cff31917b176c53aab88d9b22b816ad684ff906d1af856e146a4e3167beb",
|
||||
"clothes-female-feet.blend": "f07ea8aa5145137297b3b556a63e93ba8effa87fb123abb99754925553e0ace1",
|
||||
"clothes-female-hair.blend": "d0aa11db762f9f6974949e4cc579f66c211d7fc913def2c77b71a0ad9199f7bc",
|
||||
"clothes-female-top.blend": "0115190c186d740ac54527dbba944e7b2f0f448f4fa6bff89f3aaba2a02d1439",
|
||||
"edited-normal-female.blend": "b62d4160656b57752cb97fc81c517007c689ad93c510ef2ebc869ac410d13b51"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419277
|
||||
},
|
||||
"elder-male": {
|
||||
"age": "elder",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-male.blend",
|
||||
"animationSourceHash": "a495ad4c4f73bf9971c7f180facbca29d117e002b258380ac946274c5e7b9fee",
|
||||
"outputHashes": {
|
||||
"clothes-elder-male-bottom.blend": "1e5dcf7d41f8f3fe5c3716342a2f9d7163558492f8e4ecab4e37e7aaa0b6d6aa",
|
||||
"clothes-elder-male-feet.blend": "60af548ee9845a29df7021f92378032d961c28108503f39f9a97640bf80f1024",
|
||||
"clothes-elder-male-hair.blend": "f62f93f4464b41d1b27ed89a39d660e3aa1cc6ee1abfbe52c9e5620ffd838616",
|
||||
"clothes-elder-male-top.blend": "a8c422ae1dbcad1f4672578f68c7577b5dff198009968f0689e036090d485e67",
|
||||
"edited-normal-elder-male.blend": "93554b09a4653357b4b91682e27bdda988421ded9eca60902a530537c18c4d5f"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-elder-male.md",
|
||||
"reportJson": "conversion-report-elder-male.json",
|
||||
"sex": "male",
|
||||
"sourceHashes": {
|
||||
"clothes-male-bottom.blend": "950298780654bd908c15045d8afe572de755ddb1aa38e548c1dfb912b1cc059c",
|
||||
"clothes-male-feet.blend": "f720ce5cd154ae967673dbc2c6a93a25640dad56760586d205a18ec7b550e39c",
|
||||
"clothes-male-hair.blend": "2b267783d2f0dc991c7edd972b6c21b344abf3b5a46737c664912feae0ca9549",
|
||||
"clothes-male-top.blend": "4ff80187a33acc2a4f6228dd6492b84aee342b22a18c68d2f4c9be9fb15ec9cf",
|
||||
"edited-normal-male.blend": "4cba5e5da1e22eeedd6962d24edb1ef6ecf56c9b719c2f623ee3983c235560ff"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419267
|
||||
},
|
||||
"teenager-female": {
|
||||
"age": "teenager",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-female.blend",
|
||||
"animationSourceHash": "6b258336b456f84f42536bef39e1902bca50cc2cb45aa7dbaa0550c83365f92d",
|
||||
"outputHashes": {
|
||||
"clothes-teenager-female-bottom.blend": "77ff890f2edaa894014c88bf1e1047335182d166bc39ac48746c1dac3d374f1a",
|
||||
"clothes-teenager-female-feet.blend": "b2fed95f17586630e331642f1afa33a6d63ba1ed58758be68a5526082d520809",
|
||||
"clothes-teenager-female-hair.blend": "0c5a762e74f95bd7b10679fa986133247125e46cc33f9a08dfeeed47f3f7719b",
|
||||
"clothes-teenager-female-top.blend": "ce617dbb60927fa81a530019bfd0b822743ac57b7dd96d894c5144a2803e3335",
|
||||
"edited-normal-teenager-female.blend": "1cfb42a0086b0704a1eb9f17ee027ae01ccf4be5664f126aa7d776398b8e3f91"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-teenager-female.md",
|
||||
"reportJson": "conversion-report-teenager-female.json",
|
||||
"sex": "female",
|
||||
"sourceHashes": {
|
||||
"clothes-female-bottom.blend": "c7a5cff31917b176c53aab88d9b22b816ad684ff906d1af856e146a4e3167beb",
|
||||
"clothes-female-feet.blend": "f07ea8aa5145137297b3b556a63e93ba8effa87fb123abb99754925553e0ace1",
|
||||
"clothes-female-hair.blend": "d0aa11db762f9f6974949e4cc579f66c211d7fc913def2c77b71a0ad9199f7bc",
|
||||
"clothes-female-top.blend": "0115190c186d740ac54527dbba944e7b2f0f448f4fa6bff89f3aaba2a02d1439",
|
||||
"edited-normal-female.blend": "b62d4160656b57752cb97fc81c517007c689ad93c510ef2ebc869ac410d13b51"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419291
|
||||
},
|
||||
"teenager-male": {
|
||||
"age": "teenager",
|
||||
"animationSource": "/media/slapin/library/ogre3/ogre-projects/world2/build-vscode/assets/blender/vrm-vroid-normal-male.blend",
|
||||
"animationSourceHash": "a495ad4c4f73bf9971c7f180facbca29d117e002b258380ac946274c5e7b9fee",
|
||||
"outputHashes": {
|
||||
"clothes-teenager-male-bottom.blend": "9547a808429d71da27fdb11d58f69a2c18e6ad3f4fe78b5348dd25050133ad37",
|
||||
"clothes-teenager-male-feet.blend": "33d32f57c79f5a7b8e9c701c5e0aeffdd052a75b92ea7de234128c418e8be332",
|
||||
"clothes-teenager-male-hair.blend": "fc9c767afe4291c8a4634c2c1fc886c42d0136b905cc2288d3ca363a45957e99",
|
||||
"clothes-teenager-male-top.blend": "7b14973503c70681acbcaf66d7712a5e0ed9336875e4780d84ec749525624161",
|
||||
"edited-normal-teenager-male.blend": "d476061b1ee14df740bb052b82a2be5938246c05fcaf2df1dcded73ca1196daa"
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"report": "conversion-report-teenager-male.md",
|
||||
"reportJson": "conversion-report-teenager-male.json",
|
||||
"sex": "male",
|
||||
"sourceHashes": {
|
||||
"clothes-male-bottom.blend": "950298780654bd908c15045d8afe572de755ddb1aa38e548c1dfb912b1cc059c",
|
||||
"clothes-male-feet.blend": "f720ce5cd154ae967673dbc2c6a93a25640dad56760586d205a18ec7b550e39c",
|
||||
"clothes-male-hair.blend": "2b267783d2f0dc991c7edd972b6c21b344abf3b5a46737c664912feae0ca9549",
|
||||
"clothes-male-top.blend": "4ff80187a33acc2a4f6228dd6492b84aee342b22a18c68d2f4c9be9fb15ec9cf",
|
||||
"edited-normal-male.blend": "4cba5e5da1e22eeedd6962d24edb1ef6ecf56c9b719c2f623ee3983c235560ff"
|
||||
},
|
||||
"state": "auto-seed",
|
||||
"updated": 1793419282
|
||||
}
|
||||
},
|
||||
"updated": 1793419320,
|
||||
"version": 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,37 +20,94 @@ cmake --build build-vscode --target edited-blends # base bodies + animations
|
||||
cmake --build build-vscode # full pipeline incl. clothes
|
||||
```
|
||||
|
||||
Pipeline per sex (`male` / `female`):
|
||||
All Blender steps are wrapped in `${BLENDER_LOCKED}` (`flock -x
|
||||
${BLENDER_LOCK} <blender>`). This makes `-j` builds safe: Blender jobs
|
||||
are serialized because Blender 3.6 shares user config/addon state and the
|
||||
scripts touch common build files, while CMake copies/compiles in
|
||||
parallel. Configure fails if `flock` is missing. The lock defaults to
|
||||
`<build-dir>/blender-assets.lock`; pass the same `-DBLENDER_LOCK=...` to
|
||||
concurrent build trees. Never call `${BLENDER}` directly in a custom
|
||||
command; use `${BLENDER_LOCKED}`.
|
||||
|
||||
1. `edited-normal-<sex>.blend` is the base body. `copy_animations.py`
|
||||
copies the Mixamo animation library (`assets/blender/mixamo/`) into
|
||||
it — idle, walking, running, swimming, treading_water, sitting,
|
||||
sitting-ground, sitting-vehicle, ...
|
||||
2. Each `clothes-<sex>-<slot>.blend` holds garment meshes for one slot.
|
||||
Canonical age bands are `baby`, `child`, `teenager`, `adult` and `elder`.
|
||||
Adult keeps the legacy stem `male`/`female`; every new band uses
|
||||
`<age>-<sex>` (for example `child-male`, `elder-female`). The new base
|
||||
bodies and their clothes are generated once from the adult assets by
|
||||
`generate_age_blends.py` + the `agegen/` package (rest-pose rig scaling,
|
||||
weighted mesh/shape-key deformation, garment/hair conversion and animation
|
||||
retargeting). The generated `.blend` files are committed seeds; the normal
|
||||
build consumes them as-is and never re-runs the generator or retargeting.
|
||||
See `AGES-PLAN.md` for the conversion/hand-off contract and
|
||||
`generate_age_blends.py --check` for seed verification.
|
||||
|
||||
Pipeline per stem (`male`, `female`, `baby-male`, `child-female`, ...):
|
||||
|
||||
1. `edited-normal-<stem>.blend` is the base body. For **adult**,
|
||||
`copy_animations.py` copies the Mixamo animation library
|
||||
(`assets/blender/mixamo/`) into it. New age seeds already contain
|
||||
retargeted actions and NLA tracks.
|
||||
2. Each `clothes-<age>-<sex>-<slot>.blend` (adult:
|
||||
`clothes-<sex>-<slot>.blend`) holds garment meshes for one slot.
|
||||
`process_clothes.py` fits them to the body (weight transfer from the
|
||||
reference body parts, seam fixes), `combine_clothes.py` bakes the
|
||||
combined meshes for garment combinations (see §3).
|
||||
3. `consolidate.py` appends everything into
|
||||
`edited-normal-<sex>-consolidated.blend`.
|
||||
3. `consolidate.py` appends each clothing stage and leaves exactly one
|
||||
body armature. The clothes-only result is
|
||||
`edited-normal-<stem>-consolidated-clothes.blend`. If
|
||||
`nude-<stem>.blend` exists, a final consolidation appends its
|
||||
detailed-nude parts; otherwise the clothes-only blend is copied.
|
||||
Either way the final output is
|
||||
`edited-normal-<stem>-consolidated.blend`.
|
||||
4. `export_models2.py` exports the consolidated blend through
|
||||
blender2ogre: one `.mesh` per catalog mesh object, the shared
|
||||
`.skeleton`, `.material` files, the texture atlases
|
||||
(`clothes-atlas.png`, `clothes-atlas-normal.png`, `face-atlas.png`),
|
||||
`.skeleton`, `.material` files, the prefixed texture atlases,
|
||||
a `.glb` preview, and one **`body_part_<mesh data name>.json`
|
||||
catalog entry per mesh object** carrying the required custom
|
||||
properties (see §2).
|
||||
|
||||
Outputs land in `build-vscode/characters/<sex>/` and are staged into
|
||||
Outputs land in `build-vscode/characters/<stem>/` and are staged into
|
||||
the editScene runtime `characters/` directory (OGRE resource group
|
||||
`Characters`). The engine discovers catalog entries by scanning every
|
||||
`Characters`; every stem directory is registered as a flat resource
|
||||
location). The engine discovers catalog entries by scanning every
|
||||
resource group for `body_part_*.json` — no manual registration step.
|
||||
All resource basenames in the tree must be unique; textures are prefixed
|
||||
with `<stem>-` during export.
|
||||
|
||||
**blender2ogre names `.mesh` files after the mesh DATA name** (not the
|
||||
object name) — set both, and never reuse another blend's mesh data
|
||||
names: everything exports into the same directory and silently
|
||||
overwrites.
|
||||
|
||||
## 2. Catalog entry contract (custom properties on the MESH OBJECT)
|
||||
## 1.1 Age seeds (baby / child / teenager / elder)
|
||||
|
||||
`generate_age_blends.py` (with the `agegen/` package) creates first-pass
|
||||
seeds from the same-sex adult base and the adult wardrobe:
|
||||
|
||||
- the age rig keeps every adult bone name, hierarchy, roll and rest
|
||||
orientation; joints/segment lengths and radial thickness are scaled by
|
||||
`age_profiles.py`;
|
||||
- body, face, hair, clothes, helpers and shape keys are deformed by the
|
||||
weighted rest-pose matrices `D_i = F_age · S_i · F_adult⁻¹`;
|
||||
- animations are copied from the Mixamo library and their location/root
|
||||
tracks are scaled by the bone rest-offset ratios; rotations are untouched;
|
||||
- the result is written as `edited-normal-<age>-<sex>.blend` plus
|
||||
`clothes-<age>-<sex>-{top,bottom,feet,hair}.blend`, a conversion report
|
||||
and `AGE-GENERATION.json`.
|
||||
|
||||
Generated seeds are committed and marked `agegen_state = auto-seed`. The
|
||||
normal build must not run the generator or the retarget step again; the
|
||||
retarget step would double-scale root motion. Seeds with
|
||||
`agegen_state = hand-edited` (or a changed file hash) are protected from
|
||||
regeneration; use `--force` only when intentionally replacing them.
|
||||
`generate_age_blends.py --check` verifies the seed hashes and
|
||||
`validate_age_assets.py` verifies the built catalog/skeleton/resource tree.
|
||||
|
||||
The automatic seed is a **complete editable base**, not final art. Baby and
|
||||
child faces/silhouettes are expected to need a manual detail pass; the
|
||||
conversion report lists the first objects to inspect (stretch, seam,
|
||||
penetration, weight and animation issues).
|
||||
|
||||
|
||||
|
||||
`export_models2.py` writes a catalog JSON for every mesh object that
|
||||
carries all three required custom properties (Properties panel ->
|
||||
@@ -58,7 +115,7 @@ Object -> Custom Properties):
|
||||
|
||||
| Property | Type | Meaning |
|
||||
|----------|------|---------|
|
||||
| `age` | string | age band, currently only `adult` exists |
|
||||
| `age` | string | canonical band: `baby` / `child` / `teenager` / `adult` / `elder` (aliases `infant`, `teen`, `senior` normalize on load) |
|
||||
| `sex` | string | `male` / `female` |
|
||||
| `slot` | string | `top` / `bottom` / `feet` / `hair` / `face` |
|
||||
| `layer` | int | 0 = nude base, 1 = underwear (and hair), 2 = clothes (default 0) |
|
||||
@@ -105,45 +162,71 @@ Resolution per slot (`resolveMesh`), given the record's
|
||||
`combine_clothes.py` — a new garment only needs its own entry, the
|
||||
combinations are generated.
|
||||
- **outfitLevel 1 (underwear)**: the selected layer-1 mesh.
|
||||
- **outfitLevel 0 (nude)**: the layer-0 entry tagged
|
||||
**`detailed-nude`** when one exists for the slot (phase 6.C.1) —
|
||||
the plain layer-0 meshes are simplified composition bases meant to
|
||||
disappear under garments and lack anatomical detail. Author at most
|
||||
one `detailed-nude` variant per slot per sex (`top`/`bottom`/`feet`);
|
||||
slots without one keep the composition base, so the variants can be
|
||||
added one slot at a time. Tag it via `clothing_tags =
|
||||
"detailed-nude"` on a layer-0 mesh.
|
||||
- **No layer-1/2 garment resolved (nude slot)**: the layer-0 entry
|
||||
tagged **`detailed-nude`** when one exists for the slot (phase
|
||||
6.C.1) — the plain layer-0 meshes are simplified composition bases
|
||||
meant to disappear under garments and lack anatomical detail. This
|
||||
applies to `outfitLevel 0` and to `outfitLevel 1/2` records whose
|
||||
garment selections are empty or point at a missing catalog entry.
|
||||
Slots without a detailed variant keep the composition base, so the
|
||||
variants can be authored one slot at a time. Tag it via
|
||||
`clothing_tags = "detailed-nude"` on a layer-0 mesh.
|
||||
- Final fallback: the shortest-named layer-0 entry (Blender `.001`
|
||||
duplicates are penalised), then any entry.
|
||||
|
||||
### Authoring a `detailed-nude` part (whole-body-part replacement)
|
||||
### Detailed-nude source files (`nude-<stem>.blend`)
|
||||
|
||||
Detailed-nude parts live in an optional, separate source blend per
|
||||
age/sex stem: `nude-<stem>.blend` (`nude-male.blend`,
|
||||
`nude-baby-female.blend`, ...). The file can contain up to three
|
||||
complete body parts, one per slot: `top`, `bottom`, `feet`. It is
|
||||
optional and may be populated one slot at a time; slots without a part
|
||||
fall back to the composition bases. The normal build appends the file
|
||||
**after** all clothing stages, so `combine_clothes.py` never sees these
|
||||
parts and cannot delete or combine them.
|
||||
|
||||
A detailed-nude mesh is a COMPLETE standalone body part that replaces
|
||||
the simplified composition base at runtime when the character wears
|
||||
nothing. It is NOT built from the base part by the pipeline and no
|
||||
geometry is ever deleted from it:
|
||||
the simplified composition base at runtime when the slot has no
|
||||
clothes. It is NOT built from the base part by the pipeline and no
|
||||
geometry is ever deleted from it. The plain base part must stay in
|
||||
`edited-normal-<stem>.blend` — garments still need it as their
|
||||
combination target.
|
||||
|
||||
- The hidden-geometry deletion (`combine_clothes.py`) only runs on
|
||||
copies of the base meshes a garment explicitly names in its
|
||||
`ref_part` property. No garment references your new mesh, so it
|
||||
passes through the pipeline untouched, and the plain base part must
|
||||
stay in the blend — garments still need it as their combination
|
||||
target.
|
||||
- The replacement happens at mesh-resolution time only: outfitLevel 0
|
||||
shows the detailed part, outfitLevel >= 1 goes back to the
|
||||
composition bases/combined meshes.
|
||||
#### Seeding the file from the existing base parts
|
||||
|
||||
Steps:
|
||||
The quickest start is the seeding helper, which copies the existing
|
||||
layer-0 `BodyTop` / `BodyBottom` / `BodyFeet` parts into a new
|
||||
`nude-<stem>.blend`, renames them `*_detailed`, strips clothing
|
||||
metadata and tags them `detailed-nude`:
|
||||
|
||||
1. Model the part as a NEW mesh object in the base body blend
|
||||
`edited-normal-<sex>.blend`, next to the existing base parts
|
||||
(`BodyTop`/`BodyBottom`/...). One detailed variant per slot per
|
||||
sex at most (`top`/`bottom`/`feet`); the engine takes the first
|
||||
`detailed-nude` entry it finds for the slot.
|
||||
2. Skin it to the SAME `mixamorig:` armature with the same bind pose,
|
||||
max 4 bone influences per vertex. (`consolidate.py` re-parents it
|
||||
to the armature named by its `sex` property and adds an Armature
|
||||
modifier if missing, but the weights are your job.)
|
||||
```bash
|
||||
# Recommended: goes through the Blender lock.
|
||||
cmake --build build-vscode --target init-nude-male # adult male
|
||||
cmake --build build-vscode --target init-nude-child-female
|
||||
```
|
||||
|
||||
Or run the helper directly (stop parallel asset builds first):
|
||||
|
||||
```bash
|
||||
blender -b -Y -P assets/blender/characters/make_nude_blend.py -- \
|
||||
--age adult --sex male
|
||||
```
|
||||
|
||||
The helper refuses to overwrite an existing file unless `--force` is
|
||||
passed. It includes the body armature for editing and skinning; the
|
||||
pipeline keeps one body armature during consolidation. If you
|
||||
regenerate an age seed with `--force`, its base boundaries can change;
|
||||
re-seed or adjust the matching `nude-<stem>.blend` afterwards.
|
||||
|
||||
#### Editing requirements
|
||||
|
||||
1. Keep or replace the `BodyTop_detailed` / `BodyBottom_detailed` /
|
||||
`BodyFeet_detailed` meshes. At most one `detailed-nude` entry per
|
||||
slot per stem; the engine takes the first entry it finds.
|
||||
2. Skin each part to the SAME `mixamorig:` armature with the same bind
|
||||
pose, max 4 bone influences per vertex. (`consolidate.py`
|
||||
re-parents it to the armature named by its `sex` property and adds
|
||||
an Armature modifier if missing, but the weights are your job.)
|
||||
3. Match the boundary loops of the base part it replaces (neck, waist,
|
||||
wrists, ankles) vertex-for-vertex in position: the consolidate
|
||||
step's seam fixer unifies shape-key offsets and normals for
|
||||
@@ -152,17 +235,38 @@ Steps:
|
||||
against the face/hands/neighbouring slots.
|
||||
4. Copy the same shape keys (`fat`, ...) onto it — helper scripts in
|
||||
this directory: `transfer_shape_keys.py`,
|
||||
`propagate_shape_keys.py`, `add_missing_shape_keys.py`.
|
||||
`propagate_shape_keys.py`, `add_missing_shape_keys.py`. The seeding
|
||||
helper already does this from `Body_shapes`, and the final
|
||||
consolidation runs `add_missing_shape_keys.py` as a safety net, so a
|
||||
manually authored part also receives the missing keys automatically.
|
||||
5. Custom properties on the mesh OBJECT: `age`/`sex`/`slot` as usual,
|
||||
`layer` = 0, `clothing_tags` = `detailed-nude`, no `garments`.
|
||||
6. Give object AND mesh data a unique name (e.g.
|
||||
`male_BodyTop_detailed`); the mesh data name becomes the `.mesh`
|
||||
file name.
|
||||
7. Rebuild (`edited-blends` target or full build). No CMake or code
|
||||
changes are needed: consolidate/export discover the part by its
|
||||
custom properties and emit `body_part_<mesh data name>.json`
|
||||
automatically, and the runtime starts using it for nude characters
|
||||
of that slot.
|
||||
The seeding helper sets these automatically.
|
||||
6. Give object AND mesh data unique names (e.g. `BodyTop_detailed`);
|
||||
the mesh data name becomes the exported `.mesh` name prefixed with
|
||||
the stem (`male_BodyTop_detailed.mesh`).
|
||||
7. Rebuild the character (`character-<stem>`, `characters-all`, or the
|
||||
full build). No CMake edits are needed: `CMakeLists.txt` detects
|
||||
`nude-<stem>.blend`, appends it after the clothing consolidation and
|
||||
exports the parts as layer-0 catalog entries. If the file was added
|
||||
while CMake was already configured, the `CONFIGURE_DEPENDS` glob
|
||||
re-runs CMake on the next build (or run `cmake -B build-vscode -S .`
|
||||
manually).
|
||||
8. Verify with `cmake --build build-vscode --target
|
||||
character-assets-check`: when a `nude-<stem>.blend` exists, the
|
||||
validator requires at least one `detailed-nude` catalog entry, checks
|
||||
that every entry is layer 0 with no `garments` and matching
|
||||
age/sex, and warns about missing slots.
|
||||
|
||||
Do not add detailed-nude meshes to `edited-normal-<stem>.blend` for the
|
||||
new workflow. The separate file is what keeps them out of the clothing
|
||||
combination/removal path and lets them be replaced independently. Older
|
||||
assets with detailed-nude meshes already in the base blend still resolve
|
||||
at runtime; they simply do not get the separate-file workflow.
|
||||
|
||||
The current LOD2/3 whole-character impostors still use the default
|
||||
dressed outfit. Detailed-nude parts are visible at LOD0/LOD1; a nude
|
||||
impostor variant is future work.
|
||||
|
||||
The town population picks looks deterministically per NPC:
|
||||
`fnv1a(npcKey|slot)` indexes into the layer-1 hair list and (when
|
||||
@@ -231,12 +335,12 @@ hysteresis on every boundary):
|
||||
- **LOD0 (< 10 m)**: modular, full detail — the normal catalog parts.
|
||||
- **LOD1 (10-30 m)**: per-part `<mesh base>_lod1.mesh` variants — same
|
||||
geometry, shape keys removed, UVs remapped into one shared 2048x2048
|
||||
atlas (`charlod_<sex>-atlas.png`) with a single `charlod_<sex>`
|
||||
material per sex. Hair slots are skipped (hidden/impostor-baked at
|
||||
atlas (`charlod_<stem>-atlas.png`) with a single `charlod_<stem>`
|
||||
material per stem. Hair slots are skipped (hidden/impostor-baked at
|
||||
distance anyway). Parts without a `_lod1` variant keep their base
|
||||
mesh, so the scheme degrades gracefully.
|
||||
- **LOD2 (30-70 m)**: whole-character impostors
|
||||
`charlod_<sex>_thin_lod2.mesh` / `charlod_<sex>_fat_lod2.mesh` — the
|
||||
`charlod_<stem>_thin_lod2.mesh` / `charlod_<stem>_fat_lod2.mesh` — the
|
||||
deterministic default outfit (per slot: highest layer, first entry by
|
||||
name) joined into ONE mesh on the shared armature, `fat` shape key
|
||||
baked at 0/1, hair rigid-bound to `mixamorig:Head`, atlas material.
|
||||
@@ -250,16 +354,16 @@ hysteresis on every boundary):
|
||||
All part entities are hidden; the master entity keeps animating the
|
||||
shared skeleton and the impostor follows it via
|
||||
`shareSkeletonInstanceWith`. Hair physics (ragdoll) is OFF.
|
||||
- **LOD3 (70-100 m, till despawn)**: `charlod_<sex>_<variant>_lod3.mesh` —
|
||||
- **LOD3 (70-100 m, till despawn)**: `charlod_<stem>_<variant>_lod3.mesh` —
|
||||
collapse-decimated LOD2, ~400 triangles (hard limit 400).
|
||||
- Interiors need only LOD0/LOD1.
|
||||
|
||||
Generation is automated in the export pipeline:
|
||||
`assets/blender/characters/generate_lods.py` runs per sex on the
|
||||
consolidated blend (CMake step `edited-normal-<sex>-lods.blend`),
|
||||
`assets/blender/characters/generate_lods.py` runs per stem on the
|
||||
consolidated blend (CMake step `edited-normal-<stem>-lods.blend`),
|
||||
`export_models2.py` appends the result via its 6th argument and exports
|
||||
the LOD meshes with the SAME shared armature. A manifest
|
||||
(`charlod_<sex>.json`: atlas file + impostor mesh names) is written for
|
||||
(`charlod_<stem>.json`: atlas file + impostor mesh names) is written for
|
||||
reference; the runtime uses name conventions + `resourceExists`, not
|
||||
the manifest. `CharacterSlotSystem::prewarmShaders` also warms the
|
||||
`*_lod1.mesh` / `charlod_*.mesh` shaders.
|
||||
@@ -303,3 +407,10 @@ contract suite:
|
||||
./demoTaxi --headless --test-motion
|
||||
./demoTaxi --headless --test-detail
|
||||
```
|
||||
|
||||
Detailed-nude verification: seed a file with `init-nude-<stem>`, rebuild
|
||||
that stem, then set a character of that age/sex to outfit level Nude (or
|
||||
clear its layer-1/2 garment selections). The editor's "Resolved:" line
|
||||
and the slot mesh should show the `*_detailed` catalog entry; clothed
|
||||
characters must keep their selected garments. `--test-ages` also checks
|
||||
the runtime preference for every stem that has a `detailed-nude` entry.
|
||||
|
||||
@@ -1,30 +1,90 @@
|
||||
project(characters)
|
||||
|
||||
set(EDITED_BLENDS_LIST
|
||||
male
|
||||
female
|
||||
)
|
||||
set(CHARACTER_AGES adult baby child teenager elder)
|
||||
set(CHARACTER_SEXES male female)
|
||||
set(CHARACTER_BUILD_AGES ${CHARACTER_AGES} CACHE STRING
|
||||
"Age bands to build (semicolon list; e.g. adult or adult;elder)")
|
||||
|
||||
# Adult keeps the legacy stem (male/female) and all existing resource names.
|
||||
function(character_stem AGE SEX OUT)
|
||||
if(AGE STREQUAL "adult")
|
||||
set(${OUT} ${SEX} PARENT_SCOPE)
|
||||
else()
|
||||
set(${OUT} "${AGE}-${SEX}" PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(character_build_blend AGE SEX OUT)
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
set(${OUT}
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${STEM}.blend"
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(character_source_blend AGE SEX OUT)
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
set(${OUT}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${STEM}.blend"
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(character_nude_source AGE SEX OUT)
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
set(${OUT}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/nude-${STEM}.blend"
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Optional detailed-nude sources: one nude-<stem>.blend per age/sex.
|
||||
# CONFIGURE_DEPENDS makes CMake re-run when a file is added/removed, so the
|
||||
# per-stem pipeline below can switch between the plain clothes-consolidated
|
||||
# blend and the extra nude-append stage without a manual reconfigure.
|
||||
file(GLOB CHARACTER_NUDE_BLENDS CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/nude-*.blend")
|
||||
|
||||
function(character_glb AGE SEX OUT)
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
set(${OUT} "${CMAKE_BINARY_DIR}/characters/${STEM}/normal-${STEM}.glb"
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(EDITED_BLEND_TARGETS)
|
||||
set(CHARACTER_GLBS)
|
||||
foreach(EDITED_BLEND ${EDITED_BLENDS_LIST})
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${EDITED_BLEND}.blend
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${EDITED_BLEND}.blend
|
||||
${CMAKE_BINARY_DIR}/assets/blender/vrm-vroid-normal-${EDITED_BLEND}.blend
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/copy_animations.py
|
||||
${CMAKE_BINARY_DIR}/assets/blender/mixamo
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-E copy ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${EDITED_BLEND}.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${EDITED_BLEND}.blend
|
||||
COMMAND ${BLENDER} -b -Y
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${EDITED_BLEND}.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/copy_animations.py --
|
||||
${CMAKE_BINARY_DIR}/assets/blender/vrm-vroid-normal-${EDITED_BLEND}.blend ${EDITED_BLEND}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
list(APPEND EDITED_BLEND_TARGETS ${CMAKE_BINARY_DIR}/assets/blender/characters/edited-normal-${EDITED_BLEND}.blend)
|
||||
list(APPEND CHARACTER_GLBS ${CMAKE_BINARY_DIR}/characters/${EDITED_BLEND}/normal-${EDITED_BLEND}.glb)
|
||||
|
||||
foreach(AGE ${CHARACTER_BUILD_AGES})
|
||||
foreach(SEX ${CHARACTER_SEXES})
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
character_source_blend(${AGE} ${SEX} BASE_SOURCE)
|
||||
character_build_blend(${AGE} ${SEX} BASE_BUILD)
|
||||
set(BASE_DEPS ${BASE_SOURCE} ${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed)
|
||||
set(BASE_COMMANDS
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${BASE_SOURCE} ${BASE_BUILD})
|
||||
if(AGE STREQUAL "adult")
|
||||
# Adult keeps the historical animation-copy step.
|
||||
list(APPEND BASE_DEPS
|
||||
${CMAKE_BINARY_DIR}/assets/blender/vrm-vroid-normal-${SEX}.blend
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/copy_animations.py
|
||||
${CMAKE_BINARY_DIR}/assets/blender/mixamo)
|
||||
list(APPEND BASE_COMMANDS
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y ${BASE_BUILD}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/copy_animations.py --
|
||||
${CMAKE_BINARY_DIR}/assets/blender/vrm-vroid-normal-${SEX}.blend ${SEX})
|
||||
else()
|
||||
# Generated seeds already contain retargeted actions + NLA
|
||||
# tracks. Never retarget here (double scaling).
|
||||
list(APPEND BASE_DEPS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generate_age_blends.py)
|
||||
endif()
|
||||
add_custom_command(
|
||||
OUTPUT ${BASE_BUILD}
|
||||
DEPENDS ${BASE_DEPS}
|
||||
${BASE_COMMANDS}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
list(APPEND EDITED_BLEND_TARGETS ${BASE_BUILD})
|
||||
character_glb(${AGE} ${SEX} GLB)
|
||||
list(APPEND CHARACTER_GLBS ${GLB})
|
||||
endforeach()
|
||||
endforeach()
|
||||
list(APPEND CHARACTER_GLBS ${CMAKE_BINARY_DIR}/characters/male/male-clothes-toprobe.glb)
|
||||
add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/assets/blender/mixamo
|
||||
@@ -41,61 +101,19 @@ set(VRM_IMPORTED_BLENDS
|
||||
#add_custom_command(
|
||||
# OUTPUT ${CHARACTER_GLBS}
|
||||
# COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
# COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py
|
||||
# COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py
|
||||
# COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
# COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CHARACTER_GLBS}
|
||||
# DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py ${VRM_IMPORTED_BLENDS} ${EDITED_BLEND_TARGETS}
|
||||
# WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
|
||||
${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
|
||||
"AUTO"
|
||||
"male"
|
||||
tmp-edited-male.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-lods.blend
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.scene
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
|
||||
${VRM_IMPORTED_BLENDS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-lods.blend
|
||||
# ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-hair.stamp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
VERBATIM
|
||||
)
|
||||
# Age/sex export commands are generated after the clothes/LOD functions are
|
||||
# defined (see the bottom of this file). The top-level export loop covers
|
||||
# adult and all generated age seeds.
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
|
||||
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
"AUTO"
|
||||
"female"
|
||||
tmp-edited-female.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-lods.blend
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.scene
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
|
||||
${VRM_IMPORTED_BLENDS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-lods.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_BINARY_DIR}/characters/male/male-clothes-toprobe.glb
|
||||
COMMAND ${BLENDER} -b -Y ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y ${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/export_clothes.py
|
||||
-- ${CMAKE_BINARY_DIR}/characters/male/male-clothes-toprobe.glb
|
||||
BodyTopRobe
|
||||
@@ -126,7 +144,7 @@ foreach(MIXAMO_FILE ${MIXAMO_FILES})
|
||||
list(APPEND VRM_SOURCE "${OUTPUT_FILE}")
|
||||
endforeach()
|
||||
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/install_addons.py
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/install_addons.py
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
DEPENDS ${CMAKE_BINARY_DIR}/assets/blender/scripts/addons/3.6/VRM_Addon_for_Blender-release.zip
|
||||
${CMAKE_BINARY_DIR}/assets/blender/scripts/addons/3.6/io_ogre.zip
|
||||
@@ -152,7 +170,7 @@ add_custom_target(install_addons ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/blender
|
||||
|
||||
#add_custom_command(OUTPUT ${VRM_IMPORTED_BLENDS}
|
||||
# COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
# COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm.py
|
||||
# COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm.py
|
||||
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${VRM_IMPORTED_BLENDS}
|
||||
# DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm.py
|
||||
# ${CMAKE_CURRENT_BINARY_DIR}/blender-assons-installed
|
||||
@@ -164,7 +182,7 @@ function(blender_import_vrm BLEND VRM EDITABLE RIG)
|
||||
get_filename_component(VRM_NAME ${VRM} NAME_WE)
|
||||
add_custom_command(OUTPUT ${BLEND}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py -- ${VRM_NAME}.vrm ${BLEND} ${EDITABLE} ${RIG}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py -- ${VRM_NAME}.vrm ${BLEND} ${EDITABLE} ${RIG}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${BLEND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${BLEND}
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
|
||||
@@ -186,9 +204,9 @@ blender_import_vrm(${CMAKE_BINARY_DIR}/assets/blender/shapes/male/vrm-vroid-norm
|
||||
|
||||
#add_custom_command(OUTPUT ${VRM_IMPORTED_BLENDS}
|
||||
# COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
|
||||
# COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm2.py jane2-dress.vrm vrm-vroid-normal-female.blend modelling-vroid-normal-female.blend female
|
||||
# COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm2.py buch1.vrm vrm-vroid-normal-male.blend modelling-vroid-normal-male.blend male
|
||||
# COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm2.py buch1-chibi.vrm shapes/male/vrm-vroid-normal-male-chibi.blend modelling-shape-male-chibi.blend male
|
||||
# COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm2.py jane2-dress.vrm vrm-vroid-normal-female.blend modelling-vroid-normal-female.blend female
|
||||
# COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm2.py buch1.vrm vrm-vroid-normal-male.blend modelling-vroid-normal-male.blend male
|
||||
# COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm2.py buch1-chibi.vrm shapes/male/vrm-vroid-normal-male-chibi.blend modelling-shape-male-chibi.blend male
|
||||
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${VRM_IMPORTED_BLENDS}
|
||||
# DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/import_vrm.py
|
||||
# ${CMAKE_CURRENT_BINARY_DIR}/blender-assons-installed
|
||||
@@ -211,31 +229,28 @@ add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/characters/shapes/male/chibi/vroid
|
||||
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/shapes/male/chibi/vroid-normal-male-chibi.glb
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/shapes/male/chibi/vroid-normal-male-chibi.glb
|
||||
${CMAKE_BINARY_DIR}/characters/shapes/male/chibi/vroid-normal-male-chibi.glb)
|
||||
add_custom_target(morph ALL DEPENDS MorphTargetsResearch ${CHARACTER_GLBS})
|
||||
|
||||
# Targets (morph/edited-blends/import_vrm and per-age characters-*) are
|
||||
# defined after the age/sex export commands at the bottom of this file.
|
||||
|
||||
add_custom_target(edited-blends ALL DEPENDS ${EDITED_BLEND_TARGETS})
|
||||
|
||||
add_custom_target(import_vrm DEPENDS ${CHARACTER_GLBS})
|
||||
|
||||
function(weight_clothes SRC)
|
||||
function(weight_clothes SRC BODY)
|
||||
get_filename_component(TARGET_NAME ${SRC} NAME_WE)
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/clothes/${TARGET_NAME}_weighted.stamp
|
||||
${CMAKE_CURRENT_BINARY_DIR}/clothes/${TARGET_NAME}_weighted.blend
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/clothes
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py -- ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_NAME}.blend ./ ${CMAKE_CURRENT_BINARY_DIR}/clothes
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py -- ${SRC} ./ ${CMAKE_CURRENT_BINARY_DIR}/clothes ${BODY}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/clothes/${TARGET_NAME}_weighted.stamp
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_CURRENT_BINARY_DIR}/clothes/${TARGET_NAME}_weighted.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${SRC}
|
||||
DEPENDS ${SRC} ${BODY}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
)
|
||||
endfunction()
|
||||
function(transfer_shape_keys SRC CLOTH DST)
|
||||
add_custom_command(
|
||||
OUTPUT ${DST}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py -- ${SRC} ${CLOTH} ${DST}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py -- ${SRC} ${CLOTH} ${DST}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${DST}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${SRC} ${CLOTH}
|
||||
@@ -291,7 +306,7 @@ function(add_clothes_combination INPUT_BLEND WEIGHTED_BLEND COMBINED_BLEND)
|
||||
add_custom_command(
|
||||
OUTPUT ${STAMP_FILE} ${COMBINED_BLEND}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${COMBINED_DIR}
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/combine_clothes.py --
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/combine_clothes.py --
|
||||
${INPUT_BLEND}
|
||||
${WEIGHTED_BLEND}
|
||||
${COMBINED_BLEND}
|
||||
@@ -348,7 +363,7 @@ function(add_blend_consolidation INPUT_BLEND COMBINED_BLEND OUTPUT_BLEND)
|
||||
${COMBINE_STAMP}
|
||||
${INPUT_BLEND}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${OUTPUT_DIR}
|
||||
COMMAND ${BLENDER} -b -Y ${INPUT_BLEND}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y ${INPUT_BLEND}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/consolidate.py --
|
||||
${COMBINED_BLEND}
|
||||
${OUTPUT_BLEND}
|
||||
@@ -425,28 +440,26 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
|
||||
)
|
||||
endfunction()
|
||||
|
||||
set(SEX_LIST "male" "female")
|
||||
foreach(SEX ${SEX_LIST})
|
||||
set(HAIR_WEIGHTED_STAMP "${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-hair_weighted.stamp")
|
||||
add_custom_command(
|
||||
OUTPUT ${HAIR_WEIGHTED_STAMP}
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-hair.blend
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/clothes
|
||||
COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py --
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-hair.blend
|
||||
./ ${CMAKE_CURRENT_BINARY_DIR}/clothes
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${HAIR_WEIGHTED_STAMP}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-hair_weighted.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMENT "Processing hair meshes (weight transfer)"
|
||||
)
|
||||
set(SLOT_LIST "bottom" "top" "feet" "hair")
|
||||
list(GET SLOT_LIST -1 LAST_SLOT)
|
||||
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-top.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-bottom.blend)
|
||||
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-feet.blend)
|
||||
endforeach()
|
||||
function(character_clothes_source AGE SEX SLOT OUT)
|
||||
if(AGE STREQUAL "adult")
|
||||
set(${OUT} ${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-${SLOT}.blend
|
||||
PARENT_SCOPE)
|
||||
else()
|
||||
set(${OUT} ${CMAKE_CURRENT_SOURCE_DIR}/clothes-${AGE}-${SEX}-${SLOT}.blend
|
||||
PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(character_clothes_name AGE SEX SLOT OUT)
|
||||
if(AGE STREQUAL "adult")
|
||||
set(${OUT} "clothes-${SEX}-${SLOT}" PARENT_SCOPE)
|
||||
else()
|
||||
set(${OUT} "clothes-${AGE}-${SEX}-${SLOT}" PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Propagate missing shape keys (like "fat") from Body_shapes to base body parts.
|
||||
# This ensures all body part meshes have consistent shape keys, preventing seams.
|
||||
@@ -458,57 +471,193 @@ function(add_shape_key_propagation INPUT_BLEND OUTPUT_BLEND)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${INPUT_BLEND} ${OUTPUT_BLEND}
|
||||
COMMAND ${BLENDER} -b -Y ${OUTPUT_BLEND}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y ${OUTPUT_BLEND}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py --
|
||||
${OUTPUT_BLEND} ${OUTPUT_BLEND}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
)
|
||||
endfunction()
|
||||
|
||||
set(SLOT_LIST "bottom" "top" "feet" "hair")
|
||||
list(GET SLOT_LIST -1 LAST_SLOT)
|
||||
# male and female clothes pipeline
|
||||
foreach (SEX ${SEX_LIST})
|
||||
add_shape_key_propagation(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-${SEX}.blend
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-shapes.blend
|
||||
)
|
||||
set(SLOT_INPUT "edited-normal-${SEX}-shapes.blend")
|
||||
foreach (SLOT ${SLOT_LIST})
|
||||
set(SLOT_OUTPUT "edited-normal-${SEX}-consolidated-${SLOT}.blend")
|
||||
if (SLOT STREQUAL LAST_SLOT)
|
||||
set(SLOT_OUTPUT "edited-normal-${SEX}-consolidated.blend")
|
||||
# Age/sex clothes, consolidation, LOD and export pipeline.
|
||||
set(ALL_CHARACTER_TARGETS)
|
||||
foreach(AGE ${CHARACTER_BUILD_AGES})
|
||||
set(AGE_TARGETS)
|
||||
foreach(SEX ${CHARACTER_SEXES})
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
character_build_blend(${AGE} ${SEX} BASE_BUILD)
|
||||
character_nude_source(${AGE} ${SEX} NUDE_SRC)
|
||||
if(EXISTS "${NUDE_SRC}")
|
||||
set(NUDE_STATE 1)
|
||||
else()
|
||||
set(NUDE_STATE 0)
|
||||
endif()
|
||||
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-${SLOT}_weighted.blend"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
|
||||
# The state file is rewritten only when its content changes. It is
|
||||
# a dependency of the final blend, so adding/removing a nude source
|
||||
# forces the final stage to re-run even when its output path stays
|
||||
# the same.
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/nude_state.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/nude-${STEM}.state"
|
||||
@ONLY)
|
||||
set(CLOTHES_CONSOLIDATED
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${STEM}-consolidated-clothes.blend")
|
||||
set(SHAPES_BLEND
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${STEM}-shapes.blend")
|
||||
set(CONSOLIDATED
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${STEM}-consolidated.blend")
|
||||
set(LODS "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${STEM}-lods.blend")
|
||||
set(OUTDIR "${CMAKE_BINARY_DIR}/characters/${STEM}")
|
||||
|
||||
add_shape_key_propagation(${BASE_BUILD} ${SHAPES_BLEND})
|
||||
|
||||
set(SLOT_INPUT "edited-normal-${STEM}-shapes.blend")
|
||||
foreach(SLOT ${SLOT_LIST})
|
||||
character_clothes_source(${AGE} ${SEX} ${SLOT} CLOTHES_SRC)
|
||||
character_clothes_name(${AGE} ${SEX} ${SLOT} CLOTHES_NAME)
|
||||
weight_clothes(${CLOTHES_SRC} ${BASE_BUILD})
|
||||
set(WEIGHTED
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/clothes/${CLOTHES_NAME}_weighted.blend")
|
||||
set(SLOT_OUTPUT
|
||||
"edited-normal-${STEM}-consolidated-${SLOT}.blend")
|
||||
if(SLOT STREQUAL LAST_SLOT)
|
||||
set(SLOT_OUTPUT
|
||||
"edited-normal-${STEM}-consolidated-clothes.blend")
|
||||
endif()
|
||||
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
|
||||
"${WEIGHTED}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}")
|
||||
set(SLOT_INPUT ${SLOT_OUTPUT})
|
||||
endforeach()
|
||||
|
||||
# Final stage: the clothes pipeline always ends at
|
||||
# *-consolidated-clothes.blend. When an optional
|
||||
# nude-<stem>.blend exists, append its layer-0 detailed-nude parts
|
||||
# after all clothing stages (so combine_clothes.py never sees or
|
||||
# removes them); otherwise stage a plain copy as *-consolidated.blend.
|
||||
set(NUDE_STATE_FILE
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/nude-${STEM}.state")
|
||||
set(FINAL_DEPS "${CLOTHES_CONSOLIDATED}" "${NUDE_STATE_FILE}")
|
||||
if(EXISTS "${NUDE_SRC}")
|
||||
list(APPEND FINAL_DEPS "${NUDE_SRC}")
|
||||
add_custom_command(
|
||||
OUTPUT ${CONSOLIDATED}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y "${CLOTHES_CONSOLIDATED}"
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/consolidate.py --
|
||||
"${NUDE_SRC}"
|
||||
"${CONSOLIDATED}"
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y "${CONSOLIDATED}"
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py --
|
||||
"${CONSOLIDATED}" "${CONSOLIDATED}"
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CONSOLIDATED}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${FINAL_DEPS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/consolidate.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMENT "Appending detailed nude parts and propagating shape keys from ${NUDE_SRC}"
|
||||
)
|
||||
else()
|
||||
add_custom_command(
|
||||
OUTPUT ${CONSOLIDATED}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CLOTHES_CONSOLIDATED}" "${CONSOLIDATED}"
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CONSOLIDATED}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${FINAL_DEPS}
|
||||
COMMENT "Staging final character blend for ${STEM}"
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${LODS}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${OUTDIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${OUTDIR}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y ${CONSOLIDATED}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/generate_lods.py --
|
||||
${AGE} ${SEX} ${OUTDIR} ${LODS}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${LODS}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${CONSOLIDATED}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generate_lods.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMENT "Generating character LOD meshes for ${STEM}"
|
||||
)
|
||||
set(SLOT_INPUT ${SLOT_OUTPUT})
|
||||
|
||||
character_glb(${AGE} ${SEX} GLB)
|
||||
add_custom_command(
|
||||
OUTPUT ${GLB}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${OUTDIR}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y
|
||||
-P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
|
||||
${CONSOLIDATED}
|
||||
${GLB}
|
||||
"AUTO"
|
||||
${STEM}
|
||||
tmp-edited-${STEM}.blend
|
||||
${LODS}
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${GLB}
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${OUTDIR}/normal-${STEM}.scene
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
|
||||
${CONSOLIDATED} ${LODS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(character-${STEM}
|
||||
DEPENDS ${GLB} ${CONSOLIDATED} ${LODS})
|
||||
list(APPEND AGE_TARGETS character-${STEM})
|
||||
list(APPEND ALL_CHARACTER_TARGETS character-${STEM})
|
||||
endforeach()
|
||||
add_custom_target(characters-${AGE} DEPENDS ${AGE_TARGETS})
|
||||
endforeach()
|
||||
|
||||
add_custom_target(characters-all DEPENDS ${ALL_CHARACTER_TARGETS})
|
||||
|
||||
# CMake lists use semicolons; the shell would split --ages on them. The
|
||||
# validator accepts commas, so pass a comma-separated list instead.
|
||||
string(REPLACE ";" "," CHARACTER_ASSETS_AGES "${CHARACTER_BUILD_AGES}")
|
||||
|
||||
# Authoring helper: seed nude-<stem>.blend from the existing layer-0
|
||||
# BodyTop/BodyBottom/BodyFeet parts. The helper refuses to overwrite an
|
||||
# existing file unless --force is passed manually; these targets are
|
||||
# opt-in and never part of the normal build.
|
||||
foreach(AGE ${CHARACTER_AGES})
|
||||
foreach(SEX ${CHARACTER_SEXES})
|
||||
character_stem(${AGE} ${SEX} STEM)
|
||||
add_custom_target(init-nude-${STEM}
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/make_nude_blend.py --
|
||||
--age ${AGE} --sex ${SEX}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMENT "Seeding nude-${STEM}.blend from the existing base parts")
|
||||
endforeach()
|
||||
endforeach()
|
||||
|
||||
# Character LOD generation (demo-taxi phase 6.C.3/6.C.4): from each
|
||||
# consolidated blend generate per-part "_lod1" atlas variants and
|
||||
# whole-character charlod impostors (LOD2 thin/fat, LOD3 <= 400 tris),
|
||||
# the shared 2048 atlas and the charlod_<sex>.json manifest. The
|
||||
# export_models2 commands above append the lods blend so the impostors
|
||||
# link the same skeleton as the body parts.
|
||||
foreach (SEX ${SEX_LIST})
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-lods.blend
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
${CMAKE_BINARY_DIR}/characters/${SEX}
|
||||
COMMAND ${BLENDER} -b -Y
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-consolidated.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/generate_lods.py --
|
||||
${SEX}
|
||||
${CMAKE_BINARY_DIR}/characters/${SEX}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-lods.blend
|
||||
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-lods.blend
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
|
||||
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-${SEX}-consolidated.blend
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generate_lods.py
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
|
||||
COMMENT "Generating character LOD meshes for ${SEX}"
|
||||
)
|
||||
endforeach()
|
||||
add_custom_target(character-assets-check
|
||||
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/validate_age_assets.py
|
||||
--characters-dir ${CMAKE_BINARY_DIR}/characters
|
||||
--source-dir ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
--ages "${CHARACTER_ASSETS_AGES}"
|
||||
DEPENDS characters-all
|
||||
COMMENT "Validating generated character age assets")
|
||||
|
||||
add_custom_target(generate-age-seeds
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/generate_age_blends.py --
|
||||
--all --write --only-unedited
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/generate_age_blends.py
|
||||
COMMENT "Generating unedited age seed blends (never touches hand-edited seeds)")
|
||||
|
||||
add_custom_target(check-age-seeds
|
||||
COMMAND ${BLENDER_LOCKED} -b -Y
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/generate_age_blends.py --
|
||||
--all --check
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/generate_age_blends.py
|
||||
COMMENT "Checking age seed presence/hashes")
|
||||
add_custom_target(morph ALL DEPENDS MorphTargetsResearch ${CHARACTER_GLBS})
|
||||
add_custom_target(edited-blends ALL DEPENDS ${EDITED_BLEND_TARGETS})
|
||||
add_custom_target(import_vrm DEPENDS ${CHARACTER_GLBS})
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
"""Age-band profiles and naming helpers for the character pipeline.
|
||||
|
||||
This module is the single asset-side source of truth for the automatic
|
||||
adult -> age conversion. It is imported by the Blender scripts and by
|
||||
``generate_age_blends.py``; the C++ runtime mirrors the small amount of data
|
||||
it needs (names, age thresholds, fallback chains and body metrics).
|
||||
|
||||
Keep this file dependency-free (stdlib only) so it can be imported outside
|
||||
Blender for validation/tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
CANONICAL_AGES = ["baby", "child", "teenager", "adult", "elder"]
|
||||
|
||||
AGE_ALIASES = {
|
||||
"infant": "baby",
|
||||
"teen": "teenager",
|
||||
"adolescent": "teenager",
|
||||
"senior": "elder",
|
||||
"old": "elder",
|
||||
"old-age": "elder",
|
||||
}
|
||||
|
||||
# Canonical band boundaries used by the population reference rules.
|
||||
BABY_MAX_YEARS = 2
|
||||
CHILD_MAX_YEARS = 12
|
||||
TEEN_MAX_YEARS = 17
|
||||
ADULT_MAX_YEARS = 64
|
||||
|
||||
# Population rules' default ranges (mirrors TownPopulation::PopulationRules).
|
||||
DEFAULT_AGE_RULES = {
|
||||
"childMinYears": 1,
|
||||
"childMaxYears": 17,
|
||||
"adultMinYears": 18,
|
||||
"adultMaxYears": 64,
|
||||
"elderMinYears": 65,
|
||||
"elderMaxYears": 90,
|
||||
}
|
||||
|
||||
# Animation names every age skeleton must expose. The underlying clips may
|
||||
# differ per band (baby crawl etc.); the names are the animation-tree contract.
|
||||
REQUIRED_ANIMATIONS = [
|
||||
"default",
|
||||
"idle",
|
||||
"walking",
|
||||
"running",
|
||||
"sitting",
|
||||
"sitting-ground",
|
||||
"sitting-vehicle",
|
||||
"swimming",
|
||||
"treading_water",
|
||||
"swimming-fast",
|
||||
"jump",
|
||||
"left_turn",
|
||||
"right_turn",
|
||||
]
|
||||
|
||||
# Per-band conversion profile.
|
||||
#
|
||||
# Length/radial values are multipliers over the same-sex adult bone segments /
|
||||
# thickness. They are conversion targets, not medical proportions.
|
||||
# ``targetHeight`` is filled by sex below.
|
||||
PROFILES = {
|
||||
"baby": {
|
||||
"hipHeight": 0.28,
|
||||
"torso": 0.30,
|
||||
"head": 0.85,
|
||||
"shoulderWidth": 0.50,
|
||||
"arm": 0.30,
|
||||
"leg": 0.26,
|
||||
"foot": 0.55,
|
||||
"hipWidth": 0.55,
|
||||
"bodyRadial": 0.55,
|
||||
"limbRadial": 0.50,
|
||||
"headBulge": 1.08,
|
||||
"walkSpeedScale": 0.0, # home-only in v1
|
||||
},
|
||||
"child": {
|
||||
"hipHeight": 0.55,
|
||||
"torso": 0.62,
|
||||
"head": 0.92,
|
||||
"shoulderWidth": 0.70,
|
||||
"arm": 0.60,
|
||||
"leg": 0.55,
|
||||
"foot": 0.75,
|
||||
"hipWidth": 0.72,
|
||||
"bodyRadial": 0.78,
|
||||
"limbRadial": 0.72,
|
||||
"headBulge": 1.03,
|
||||
"walkSpeedScale": 0.75,
|
||||
},
|
||||
"teenager": {
|
||||
"hipHeight": 0.94,
|
||||
"torso": 0.92,
|
||||
"head": 0.99,
|
||||
"shoulderWidth": 0.92,
|
||||
"arm": 0.92,
|
||||
"leg": 0.94,
|
||||
"foot": 0.97,
|
||||
"hipWidth": 0.92,
|
||||
"bodyRadial": 0.94,
|
||||
"limbRadial": 0.93,
|
||||
"headBulge": 1.0,
|
||||
"walkSpeedScale": 0.95,
|
||||
},
|
||||
"adult": {
|
||||
"hipHeight": 1.0,
|
||||
"torso": 1.0,
|
||||
"head": 1.0,
|
||||
"shoulderWidth": 1.0,
|
||||
"arm": 1.0,
|
||||
"leg": 1.0,
|
||||
"foot": 1.0,
|
||||
"hipWidth": 1.0,
|
||||
"bodyRadial": 1.0,
|
||||
"limbRadial": 1.0,
|
||||
"headBulge": 1.0,
|
||||
"walkSpeedScale": 1.0,
|
||||
},
|
||||
# Elder v1 stays bind-identical to adult: aging is geometry/texture only.
|
||||
# The manual detail pass can scale the rig or posture later.
|
||||
"elder": {
|
||||
"hipHeight": 1.0,
|
||||
"torso": 1.0,
|
||||
"head": 1.0,
|
||||
"shoulderWidth": 1.0,
|
||||
"arm": 1.0,
|
||||
"leg": 1.0,
|
||||
"foot": 1.0,
|
||||
"hipWidth": 1.0,
|
||||
"bodyRadial": 1.0,
|
||||
"limbRadial": 1.0,
|
||||
"headBulge": 1.0,
|
||||
"walkSpeedScale": 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
# Target mesh/skeleton heights in metres. Elder is adult height for the
|
||||
# bind-identical v1 seed; the detail pass can shorten it.
|
||||
TARGET_HEIGHT = {
|
||||
("baby", "male"): 0.72,
|
||||
("baby", "female"): 0.70,
|
||||
("child", "male"): 1.20,
|
||||
("child", "female"): 1.18,
|
||||
("teenager", "male"): 1.70,
|
||||
("teenager", "female"): 1.55,
|
||||
("adult", "male"): 1.85,
|
||||
("adult", "female"): 1.58,
|
||||
("elder", "male"): 1.85,
|
||||
("elder", "female"): 1.58,
|
||||
}
|
||||
|
||||
ADULT_HEIGHT = {"male": 1.85, "female": 1.58}
|
||||
|
||||
FALLBACK_CHAINS = {
|
||||
"baby": ["baby", "child", "adult"],
|
||||
"child": ["child", "baby", "teenager", "adult"],
|
||||
"teenager": ["teenager", "child", "adult"],
|
||||
"adult": ["adult", "teenager", "elder"],
|
||||
"elder": ["elder", "adult"],
|
||||
}
|
||||
|
||||
# Garment/body fit reporting thresholds.
|
||||
GARMENT_OFFSET = 0.003 # metres pushed out from the body surface
|
||||
QUALITY = {
|
||||
"seam_warn": 0.001,
|
||||
"seam_fail": 0.01,
|
||||
"penetration_warn": 0.01,
|
||||
"penetration_fail": 0.05,
|
||||
"height_tolerance": 0.05,
|
||||
"edge_residual_warn": 0.35, # |log(expected/actual)|-ish residual
|
||||
"edge_residual_fail": 0.70,
|
||||
}
|
||||
|
||||
|
||||
def normalize_age(age: str | None) -> str:
|
||||
if not age:
|
||||
return "adult"
|
||||
key = str(age).strip().lower()
|
||||
key = AGE_ALIASES.get(key, key)
|
||||
return key if key in CANONICAL_AGES else "adult"
|
||||
|
||||
|
||||
def age_band_for_years(years: int) -> str:
|
||||
if years <= BABY_MAX_YEARS:
|
||||
return "baby"
|
||||
if years <= CHILD_MAX_YEARS:
|
||||
return "child"
|
||||
if years <= TEEN_MAX_YEARS:
|
||||
return "teenager"
|
||||
if years <= ADULT_MAX_YEARS:
|
||||
return "adult"
|
||||
return "elder"
|
||||
|
||||
|
||||
def is_working_age(years: int) -> bool:
|
||||
return age_band_for_years(years) == "adult"
|
||||
|
||||
|
||||
def fallback_chain(age: str) -> list[str]:
|
||||
return list(FALLBACK_CHAINS[normalize_age(age)])
|
||||
|
||||
|
||||
def stem(age: str, sex: str) -> str:
|
||||
age = normalize_age(age)
|
||||
return sex if age == "adult" else f"{age}-{sex}"
|
||||
|
||||
|
||||
def armature_name(age: str, sex: str) -> str:
|
||||
return stem(age, sex)
|
||||
|
||||
|
||||
def mesh_prefix(age: str, sex: str) -> str:
|
||||
return stem(age, sex) + "_"
|
||||
|
||||
|
||||
def target_height(age: str, sex: str) -> float:
|
||||
age = normalize_age(age)
|
||||
return TARGET_HEIGHT[(age, sex)]
|
||||
|
||||
|
||||
def body_metrics(age: str, sex: str) -> dict:
|
||||
"""Capsule/head metrics scaled from the current adult defaults."""
|
||||
age = normalize_age(age)
|
||||
scale = target_height(age, sex) / ADULT_HEIGHT[sex]
|
||||
radius = 0.3 * scale
|
||||
height = 1.8 * scale
|
||||
return {
|
||||
"radius": radius,
|
||||
"height": height,
|
||||
"totalHeight": height + 2.0 * radius,
|
||||
"headRadius": 0.13 * scale,
|
||||
"headOffsetY": 0.0,
|
||||
"walkSpeedScale": PROFILES[age]["walkSpeedScale"],
|
||||
}
|
||||
|
||||
|
||||
def profile(age: str) -> dict:
|
||||
return dict(PROFILES[normalize_age(age)])
|
||||
|
||||
|
||||
def profile_hash() -> str:
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
payload = {
|
||||
"ages": CANONICAL_AGES,
|
||||
"profiles": PROFILES,
|
||||
"heights": {f"{a}|{s}": h for (a, s), h in TARGET_HEIGHT.items()},
|
||||
"animations": REQUIRED_ANIMATIONS,
|
||||
"quality": QUALITY,
|
||||
}
|
||||
return hashlib.sha256(
|
||||
json.dumps(payload, sort_keys=True).encode("utf-8")
|
||||
).hexdigest()[:16]
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Age-seed conversion helpers (Blender 3.6)."""
|
||||
|
||||
__all__ = [
|
||||
"profiles",
|
||||
"rig",
|
||||
"deform",
|
||||
"report",
|
||||
"anim",
|
||||
]
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Animation copy/retargeting for age seeds.
|
||||
|
||||
The adult Mixamo clips animate rotations in bone-local space and locations for
|
||||
the hips/root. Because the age rig keeps every bone's rest orientation, the
|
||||
rotation curves transfer 1:1; only location curves need the bone's rest-offset
|
||||
ratio. This module is authoring-time only and must not run during the normal
|
||||
build (that would scale the location tracks twice).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import bpy
|
||||
|
||||
from . import profiles
|
||||
from .rig import bone_length_ratios
|
||||
|
||||
_LOC_RE = re.compile(r'pose\.bones\["([^"]+)"\]\.(location|delta_location)')
|
||||
|
||||
|
||||
def _nla_track_names(arm):
|
||||
if not arm.animation_data:
|
||||
return set()
|
||||
return {t.name for t in arm.animation_data.nla_tracks}
|
||||
|
||||
|
||||
def _ensure_track(arm, action):
|
||||
ad = arm.animation_data
|
||||
if ad is None:
|
||||
arm.animation_data_create()
|
||||
ad = arm.animation_data
|
||||
existing = {t.name for t in ad.nla_tracks}
|
||||
if action.name in existing:
|
||||
return
|
||||
track = ad.nla_tracks.new()
|
||||
track.name = action.name
|
||||
# Match copy_animations.py: strips start at frame 1 and remain active;
|
||||
# export_models2.py bakes each strip explicitly.
|
||||
track.strips.new(action.name, 1, action)
|
||||
|
||||
|
||||
def retarget_actions(arm, ratios: dict, hip_ratio: float,
|
||||
actions) -> dict:
|
||||
"""Scale location F-curves in *actions* and return a small summary."""
|
||||
scaled_curves = 0
|
||||
for action in actions:
|
||||
for fc in action.fcurves:
|
||||
m = _LOC_RE.match(fc.data_path)
|
||||
if m:
|
||||
bone = m.group(1)
|
||||
ratio = ratios.get(bone, 1.0)
|
||||
for kp in fc.keyframe_points:
|
||||
kp.co.y *= ratio
|
||||
kp.handle_left.y *= ratio
|
||||
kp.handle_right.y *= ratio
|
||||
scaled_curves += 1
|
||||
elif fc.data_path in ("location", "delta_location"):
|
||||
for kp in fc.keyframe_points:
|
||||
kp.co.y *= hip_ratio
|
||||
kp.handle_left.y *= hip_ratio
|
||||
kp.handle_right.y *= hip_ratio
|
||||
scaled_curves += 1
|
||||
return {"scaledLocationCurves": scaled_curves}
|
||||
|
||||
|
||||
def _ensure_alias(missing: str, present: set) -> str | None:
|
||||
candidates = {
|
||||
"swimming-fast": ["swimming", "walking", "idle"],
|
||||
"sitting-ground": ["sitting", "default"],
|
||||
"sitting-vehicle": ["sitting", "default"],
|
||||
"sitting": ["default", "idle"],
|
||||
"treading_water": ["swimming", "idle"],
|
||||
"default": ["idle", "walking", "T-Pose"],
|
||||
}.get(missing, [])
|
||||
for name in candidates:
|
||||
if name in present:
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def append_and_retarget(arm, adult_arm, source_blend: str, age: str,
|
||||
sex: str, hips_bone="mixamorig:Hips"):
|
||||
"""Append the sex's Mixamo actions, retarget them and build NLA tracks."""
|
||||
age = profiles.normalize_age(age)
|
||||
stem = profiles.stem(age, sex)
|
||||
|
||||
existing_names = {a.name for a in bpy.data.actions}
|
||||
with bpy.data.libraries.load(source_blend, link=False) as (src, dst):
|
||||
wanted = [n for n in src.actions if n not in existing_names]
|
||||
dst.actions = wanted
|
||||
appended = [a for a in dst.actions if a is not None]
|
||||
|
||||
# Fill required-but-missing action names with the closest available clip.
|
||||
# These are explicit placeholders for the manual animation pass; keeping
|
||||
# the names present prevents silent T-poses / missing-state errors.
|
||||
aliased = {}
|
||||
for name in profiles.REQUIRED_ANIMATIONS:
|
||||
if bpy.data.actions.get(name) is not None:
|
||||
continue
|
||||
src_name = _ensure_alias(name, {a.name for a in bpy.data.actions})
|
||||
if not src_name:
|
||||
continue
|
||||
src = bpy.data.actions[src_name]
|
||||
new = src.copy()
|
||||
new.name = name
|
||||
new.use_fake_user = True
|
||||
aliased[name] = src_name
|
||||
|
||||
# Ensure every action (including custom baby clips and aliases) has an
|
||||
# NLA track.
|
||||
for action in bpy.data.actions:
|
||||
if action.name.startswith("bake_"):
|
||||
continue
|
||||
if action.name in ("T-Pose",):
|
||||
continue
|
||||
_ensure_track(arm, action)
|
||||
|
||||
ratios = bone_length_ratios(adult_arm, arm)
|
||||
hip_ratio = ratios.get(hips_bone, 1.0)
|
||||
|
||||
# Retarget ALL actions, including clips already present in the source
|
||||
# base blend (sitting-ground/sitting-vehicle/etc. are authored there).
|
||||
summary = retarget_actions(arm, ratios, hip_ratio,
|
||||
list(bpy.data.actions))
|
||||
summary["appended"] = [a.name for a in appended]
|
||||
summary["aliased"] = aliased
|
||||
|
||||
# Ensure the default action is active (export_models2 expects it).
|
||||
default = bpy.data.actions.get("default")
|
||||
if default is not None:
|
||||
if arm.animation_data is None:
|
||||
arm.animation_data_create()
|
||||
arm.animation_data.action = default
|
||||
|
||||
present = {a.name for a in bpy.data.actions}
|
||||
missing = [n for n in profiles.REQUIRED_ANIMATIONS if n not in present]
|
||||
summary["missing"] = missing
|
||||
summary["stem"] = stem
|
||||
return summary
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Weighted rest-pose deformation for body parts, clothes and shape keys."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from . import profiles
|
||||
|
||||
|
||||
def _nearest_bone_distance(point: Vector, bone) -> float:
|
||||
a = bone.head_local
|
||||
b = bone.tail_local
|
||||
ab = b - a
|
||||
denom = ab.length_squared
|
||||
if denom <= 1e-12:
|
||||
return (point - a).length
|
||||
t = max(0.0, min(1.0, (point - a).dot(ab) / denom))
|
||||
return (point - (a + ab * t)).length
|
||||
|
||||
|
||||
def _nearest_bone_name(point: Vector, adult_arm) -> str:
|
||||
best = None
|
||||
best_d = 1e30
|
||||
for b in adult_arm.data.bones:
|
||||
if not b.use_deform:
|
||||
continue
|
||||
d = _nearest_bone_distance(point, b)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = b.name
|
||||
return best or "Root"
|
||||
|
||||
|
||||
def _group_name_map(obj) -> dict:
|
||||
return {i: g.name for i, g in enumerate(obj.vertex_groups)}
|
||||
|
||||
|
||||
def _vertex_matrix(v, group_names, dmatrices, adult_arm, obj_to_arm):
|
||||
"""Return (matrix, influence_count) for one mesh vertex."""
|
||||
acc = 0.0
|
||||
m = Matrix(((0.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 0.0)))
|
||||
influences = 0
|
||||
for g in v.groups:
|
||||
w = float(g.weight)
|
||||
if w <= 0.0:
|
||||
continue
|
||||
name = group_names.get(g.group)
|
||||
dm = dmatrices.get(name)
|
||||
if dm is None:
|
||||
continue
|
||||
acc += w
|
||||
m = m + dm * w
|
||||
influences += 1
|
||||
if acc > 1e-9:
|
||||
return m * (1.0 / acc), influences
|
||||
# No usable weight: bind to the nearest deform bone.
|
||||
p_arm = obj_to_arm @ v.co
|
||||
name = _nearest_bone_name(p_arm, adult_arm)
|
||||
dm = dmatrices.get(name)
|
||||
return (dm.copy() if dm else Matrix.Identity(4)), 0
|
||||
|
||||
|
||||
def deform_mesh_object(obj, adult_arm, age_arm, dmatrices,
|
||||
profile: dict, retarget_parent=True):
|
||||
"""Deform one mesh and its shape keys in place.
|
||||
|
||||
Returns a metrics dict. ``retarget_parent`` re-parents the mesh to the
|
||||
age armature and points the armature modifier at it (used for the base
|
||||
body seed). Garment seeds are saved without a body armature link, so
|
||||
they pass ``False``.
|
||||
"""
|
||||
if obj.type != 'MESH':
|
||||
return None
|
||||
mesh = obj.data
|
||||
group_names = _group_name_map(obj)
|
||||
|
||||
obj_to_arm = adult_arm.matrix_world.inverted() @ obj.matrix_world
|
||||
arm_to_obj = obj.matrix_world.inverted() @ age_arm.matrix_world
|
||||
|
||||
before = [v.co.copy() for v in mesh.vertices]
|
||||
matrices = []
|
||||
unweighted = 0
|
||||
max_inf = 0
|
||||
for v in mesh.vertices:
|
||||
m, inf = _vertex_matrix(v, group_names, dmatrices, adult_arm,
|
||||
obj_to_arm)
|
||||
matrices.append(m)
|
||||
if inf == 0:
|
||||
unweighted += 1
|
||||
max_inf = max(max_inf, inf)
|
||||
|
||||
# Basis vertices.
|
||||
for i, v in enumerate(mesh.vertices):
|
||||
p = obj_to_arm @ v.co
|
||||
v.co = arm_to_obj @ (matrices[i] @ p)
|
||||
|
||||
# Non-Basis shape keys: absolute positions, same weights.
|
||||
key_count = 0
|
||||
if mesh.shape_keys:
|
||||
for kb in mesh.shape_keys.key_blocks:
|
||||
if kb.name == 'Basis':
|
||||
continue
|
||||
key_count += 1
|
||||
for i, kp in enumerate(kb.data):
|
||||
p = obj_to_arm @ kp.co
|
||||
kp.co = arm_to_obj @ (matrices[i] @ p)
|
||||
|
||||
mesh.update()
|
||||
try:
|
||||
mesh.calc_normals()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if retarget_parent:
|
||||
obj.parent = age_arm
|
||||
mod = next((m for m in obj.modifiers
|
||||
if m.type == 'ARMATURE'), None)
|
||||
if mod is None:
|
||||
mod = obj.modifiers.new(name="Armature", type='ARMATURE')
|
||||
mod.object = age_arm
|
||||
obj.matrix_parent_inverse = age_arm.matrix_world.inverted()
|
||||
|
||||
# Metrics.
|
||||
edge_ratios = []
|
||||
for e in mesh.edges:
|
||||
a, b = e.vertices
|
||||
lb = (before[a] - before[b]).length
|
||||
la = (mesh.vertices[a].co - mesh.vertices[b].co).length
|
||||
if lb > 1e-8:
|
||||
edge_ratios.append(la / lb)
|
||||
mesh.calc_loop_triangles()
|
||||
degenerate = 0
|
||||
for tri in mesh.loop_triangles:
|
||||
a, b, c = tri.vertices
|
||||
area = ((mesh.vertices[b].co - mesh.vertices[a].co).cross(
|
||||
mesh.vertices[c].co - mesh.vertices[a].co)).length * 0.5
|
||||
if area < 1e-10:
|
||||
degenerate += 1
|
||||
|
||||
return {
|
||||
"vertices": len(mesh.vertices),
|
||||
"polygons": len(mesh.polygons),
|
||||
"shapeKeys": key_count,
|
||||
"unweighted": unweighted,
|
||||
"maxInfluences": max_inf,
|
||||
"edgeRatioMin": min(edge_ratios) if edge_ratios else 1.0,
|
||||
"edgeRatioMax": max(edge_ratios) if edge_ratios else 1.0,
|
||||
"degenerateTris": degenerate,
|
||||
"before": before,
|
||||
}
|
||||
|
||||
|
||||
def deform_shape_keys_only(obj, adult_arm, age_arm, dmatrices):
|
||||
"""Transform shape keys without touching the basis (utility)."""
|
||||
return None
|
||||
|
||||
|
||||
def snap_seams(objects_with_metrics, max_report=10):
|
||||
"""Snap coincident boundary vertices across parts to their average.
|
||||
|
||||
``objects_with_metrics`` is a list of ``(obj, metrics)`` where metrics has
|
||||
the original vertex positions. Returns a report dict.
|
||||
"""
|
||||
buckets = {}
|
||||
for obj, metrics in objects_with_metrics:
|
||||
before = metrics.get("before")
|
||||
if not before:
|
||||
continue
|
||||
for i, p in enumerate(before):
|
||||
key = (round(p.x, 5), round(p.y, 5), round(p.z, 5))
|
||||
buckets.setdefault(key, []).append((obj, i, p))
|
||||
max_gap = 0.0
|
||||
snapped = 0
|
||||
examples = []
|
||||
for key, items in buckets.items():
|
||||
if len(items) < 2:
|
||||
continue
|
||||
positions = [obj.matrix_world @ obj.data.vertices[i].co
|
||||
for obj, i, _ in items]
|
||||
avg = Vector((0, 0, 0))
|
||||
for p in positions:
|
||||
avg += p
|
||||
avg /= len(positions)
|
||||
gap = max((p - avg).length for p in positions)
|
||||
if gap < 1e-6:
|
||||
continue
|
||||
max_gap = max(max_gap, gap)
|
||||
for obj, i, _ in items:
|
||||
delta = avg - (obj.matrix_world @ obj.data.vertices[i].co)
|
||||
if delta.length <= 1e-9:
|
||||
continue
|
||||
v = obj.data.vertices[i]
|
||||
v.co += obj.matrix_world.inverted().to_3x3() @ delta
|
||||
if obj.data.shape_keys:
|
||||
for kb in obj.data.shape_keys.key_blocks:
|
||||
if kb.name == 'Basis':
|
||||
continue
|
||||
kb.data[i].co += (obj.matrix_world.inverted().to_3x3()
|
||||
@ delta)
|
||||
snapped += 1
|
||||
if len(examples) < max_report:
|
||||
examples.append({
|
||||
"position": [round(v, 4) for v in key],
|
||||
"gap": round(gap, 6),
|
||||
"objects": sorted({obj.name for obj, _, _ in items}),
|
||||
})
|
||||
return {
|
||||
"coincidentGroups": sum(1 for v in buckets.values() if len(v) > 1),
|
||||
"snappedVertices": snapped,
|
||||
"maxGap": max_gap,
|
||||
"examples": examples,
|
||||
}
|
||||
|
||||
|
||||
def transform_hair_uniform(obj, adult_pivot: Vector, age_pivot: Vector,
|
||||
scale: float):
|
||||
"""Uniformly scale a hair mesh and move it from adult to age scalp."""
|
||||
obj_to_arm = obj.matrix_world
|
||||
inv = obj.matrix_world.inverted()
|
||||
for v in obj.data.vertices:
|
||||
p = obj_to_arm @ v.co
|
||||
v.co = inv @ (age_pivot + (p - adult_pivot) * scale)
|
||||
if obj.data.shape_keys:
|
||||
for kb in obj.data.shape_keys.key_blocks:
|
||||
if kb.name == 'Basis':
|
||||
continue
|
||||
for kp in kb.data:
|
||||
p = obj_to_arm @ kp.co
|
||||
kp.co = inv @ (age_pivot + (p - adult_pivot) * scale)
|
||||
obj.data.update()
|
||||
try:
|
||||
obj.data.calc_normals()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Re-export the asset-side age profile data for the converter package."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
_CHARACTERS_DIR = os.path.dirname(_THIS_DIR)
|
||||
if _CHARACTERS_DIR not in sys.path:
|
||||
sys.path.insert(0, _CHARACTERS_DIR)
|
||||
|
||||
import age_profiles as ap # noqa: E402,F401
|
||||
|
||||
CANONICAL_AGES = ap.CANONICAL_AGES
|
||||
PROFILES = ap.PROFILES
|
||||
REQUIRED_ANIMATIONS = ap.REQUIRED_ANIMATIONS
|
||||
normalize_age = ap.normalize_age
|
||||
age_band_for_years = ap.age_band_for_years
|
||||
fallback_chain = ap.fallback_chain
|
||||
stem = ap.stem
|
||||
armature_name = ap.armature_name
|
||||
mesh_prefix = ap.mesh_prefix
|
||||
target_height = ap.target_height
|
||||
body_metrics = ap.body_metrics
|
||||
profile = ap.profile
|
||||
profile_hash = ap.profile_hash
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Conversion quality reporting for generated age seeds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from . import profiles
|
||||
|
||||
|
||||
class ConversionReport:
|
||||
def __init__(self, age: str, sex: str):
|
||||
self.age = profiles.normalize_age(age)
|
||||
self.sex = sex
|
||||
self.stem = profiles.stem(self.age, sex)
|
||||
self.started = time.time()
|
||||
self.objects = {}
|
||||
self.warnings = []
|
||||
self.fails = []
|
||||
self.info = {}
|
||||
self.seams = {}
|
||||
self.rig = {}
|
||||
|
||||
def add_object(self, name: str, metrics: dict, quality: str = "ok",
|
||||
note: str = ""):
|
||||
entry = dict(metrics or {})
|
||||
entry.pop("before", None) # large, not serializable/useful
|
||||
entry["quality"] = quality
|
||||
entry["note"] = note
|
||||
self.objects[name] = entry
|
||||
if quality == "fail":
|
||||
self.fails.append(name)
|
||||
elif quality in ("warn", "placeholder"):
|
||||
self.warnings.append(name)
|
||||
|
||||
def status(self):
|
||||
if self.fails:
|
||||
return "fail"
|
||||
if self.warnings:
|
||||
return "warn"
|
||||
return "ok"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"age": self.age,
|
||||
"sex": self.sex,
|
||||
"stem": self.stem,
|
||||
"profileHash": profiles.profile_hash(),
|
||||
"timestamp": int(self.started),
|
||||
"status": self.status(),
|
||||
"info": self.info,
|
||||
"rig": self.rig,
|
||||
"seams": self.seams,
|
||||
"objects": self.objects,
|
||||
"warnings": self.warnings,
|
||||
"fails": self.fails,
|
||||
}
|
||||
|
||||
def write(self, out_dir: str):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
base = os.path.join(out_dir,
|
||||
f"conversion-report-{self.age}-{self.sex}")
|
||||
data = self.to_dict()
|
||||
with open(base + ".json", "w") as f:
|
||||
json.dump(data, f, indent=2, sort_keys=True)
|
||||
|
||||
lines = [
|
||||
f"# Conversion Report — {self.age} / {self.sex}",
|
||||
"",
|
||||
f"- Stem: `{self.stem}`",
|
||||
f"- Status: **{data['status']}**",
|
||||
f"- Profile hash: `{data['profileHash']}`",
|
||||
f"- Objects: {len(self.objects)}",
|
||||
f"- Warnings: {len(self.warnings)}",
|
||||
f"- Fails: {len(self.fails)}",
|
||||
"",
|
||||
]
|
||||
if self.info:
|
||||
lines.append("## Info")
|
||||
for k in sorted(self.info):
|
||||
lines.append(f"- {k}: `{self.info[k]}`")
|
||||
lines.append("")
|
||||
if self.seams:
|
||||
lines.append("## Seams")
|
||||
for k, v in self.seams.items():
|
||||
lines.append(f"- {k}: `{v}`")
|
||||
lines.append("")
|
||||
if self.fails:
|
||||
lines += ["## Failures", ""]
|
||||
lines += [f"- `{name}`" for name in self.fails]
|
||||
lines.append("")
|
||||
if self.warnings:
|
||||
lines += ["## Warnings (first 20)", ""]
|
||||
lines += [f"- `{name}`" for name in self.warnings[:20]]
|
||||
lines.append("")
|
||||
|
||||
lines += ["## Objects", ""]
|
||||
for name in sorted(self.objects):
|
||||
e = self.objects[name]
|
||||
lines.append(
|
||||
f"- `{name}` — {e.get('quality', 'ok')}; "
|
||||
f"verts={e.get('vertices', '?')}, "
|
||||
f"tris={e.get('degenerateTris', '?')} degenerate, "
|
||||
f"influences={e.get('maxInfluences', '?')}, "
|
||||
f"unweighted={e.get('unweighted', '?')}, "
|
||||
f"edge={e.get('edgeRatioMin', 1):.2f}.."
|
||||
f"{e.get('edgeRatioMax', 1):.2f}"
|
||||
+ (f"; {e.get('note')}" if e.get("note") else "")
|
||||
)
|
||||
path = base + ".md"
|
||||
with open(path, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
return path, base + ".json"
|
||||
|
||||
|
||||
def evaluate_object_quality(metrics: dict, is_catalog: bool) -> str:
|
||||
"""Cheap per-object quality classification for the seed report."""
|
||||
if not metrics:
|
||||
return "warn"
|
||||
if metrics.get("unweighted", 0) > 0 and is_catalog:
|
||||
return "warn"
|
||||
if metrics.get("maxInfluences", 0) > 4:
|
||||
return "fail"
|
||||
if metrics.get("degenerateTris", 0) > 0:
|
||||
return "warn"
|
||||
emin = metrics.get("edgeRatioMin", 1.0)
|
||||
emax = metrics.get("edgeRatioMax", 1.0)
|
||||
# Raw ratios are expected to compress for baby/child; flag only extreme
|
||||
# local distortion. The per-bone expected ratio is evaluated in the
|
||||
# report writer's caller when available.
|
||||
if emax > 2.5 or (emin > 0.0 and emin < 0.35):
|
||||
return "warn"
|
||||
return "ok"
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Age-rig construction and rest-pose transform data.
|
||||
|
||||
All matrices are in armature space. The age rig keeps the adult bone names,
|
||||
hierarchy, roll and orientation; only joint positions and segment lengths
|
||||
change. This is what makes rotation-only animation retargeting valid.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from . import profiles
|
||||
|
||||
|
||||
def bone_region(name: str) -> dict:
|
||||
"""Return length/radial/offset profile keys for a Mixamo bone name."""
|
||||
n = name
|
||||
low = n.lower()
|
||||
|
||||
if n in ("Root", "mixamorig:Hips"):
|
||||
return {"length": "hipHeight", "radial": "bodyRadial",
|
||||
"offset": "hipHeight"}
|
||||
if "spine" in low:
|
||||
return {"length": "torso", "radial": "bodyRadial",
|
||||
"offset": "torso"}
|
||||
if "neck" in low:
|
||||
return {"length": "torso", "radial": "bodyRadial",
|
||||
"offset": "torso"}
|
||||
if ("head" in low or "jaw" in low or "eye" in low or
|
||||
"teeth" in low or "tongue" in low):
|
||||
return {"length": "head", "radial": "head", "offset": "head"}
|
||||
if "bust" in low:
|
||||
return {"length": "torso", "radial": "bodyRadial",
|
||||
"offset": "torso"}
|
||||
if "hair" in low or "j_sec_hair" in low:
|
||||
return {"length": "head", "radial": "head", "offset": "head"}
|
||||
if "sleeve" in low:
|
||||
return {"length": "arm", "radial": "limbRadial",
|
||||
"offset": "arm"}
|
||||
if "skirt" in low:
|
||||
return {"length": "leg", "radial": "bodyRadial",
|
||||
"offset": "hipWidth"}
|
||||
if "shoulder" in low:
|
||||
return {"length": "shoulderWidth", "radial": "limbRadial",
|
||||
"offset": "shoulderWidth"}
|
||||
if "upleg" in low or "upperleg" in low:
|
||||
return {"length": "leg", "radial": "limbRadial",
|
||||
"offset": "hipWidth"}
|
||||
if "leg" in low:
|
||||
return {"length": "leg", "radial": "limbRadial",
|
||||
"offset": "hipWidth"}
|
||||
if "foot" in low:
|
||||
return {"length": "foot", "radial": "limbRadial",
|
||||
"offset": "leg"}
|
||||
if "toe" in low:
|
||||
return {"length": "foot", "radial": "limbRadial",
|
||||
"offset": "foot"}
|
||||
if "arm" in low or "hand" in low or "thumb" in low or \
|
||||
"index" in low or "middle" in low or "ring" in low or \
|
||||
"pinky" in low:
|
||||
return {"length": "arm", "radial": "limbRadial",
|
||||
"offset": "arm"}
|
||||
return {"length": "torso", "radial": "bodyRadial", "offset": "torso"}
|
||||
|
||||
|
||||
def _depth(bone) -> int:
|
||||
d = 0
|
||||
p = bone.parent
|
||||
while p is not None:
|
||||
d += 1
|
||||
p = p.parent
|
||||
return d
|
||||
|
||||
|
||||
def _unique_name(existing, base):
|
||||
if base not in existing:
|
||||
return base
|
||||
i = 1
|
||||
while f"{base}.{i:03d}" in existing:
|
||||
i += 1
|
||||
return f"{base}.{i:03d}"
|
||||
|
||||
|
||||
def build_age_armature(adult_arm, age: str, sex: str,
|
||||
profile: dict | None = None,
|
||||
collection=None):
|
||||
"""Duplicate *adult_arm* and scale its rest pose into the age rig.
|
||||
|
||||
Returns the new armature object. The new object/data are named
|
||||
``<age>-<sex>`` (adult would keep the legacy sex name; this function is
|
||||
only used for non-adult seeds).
|
||||
"""
|
||||
age = profiles.normalize_age(age)
|
||||
profile = profile or profiles.profile(age)
|
||||
stem = profiles.stem(age, sex)
|
||||
|
||||
age_arm = adult_arm.copy()
|
||||
age_arm.data = adult_arm.data.copy()
|
||||
age_arm.name = stem
|
||||
age_arm.data.name = stem
|
||||
(collection or bpy.context.collection).objects.link(age_arm)
|
||||
|
||||
# Snapshot adult rest positions before entering edit mode.
|
||||
adult_head = {b.name: b.head_local.copy() for b in adult_arm.data.bones}
|
||||
adult_tail = {b.name: b.tail_local.copy() for b in adult_arm.data.bones}
|
||||
|
||||
bpy.context.view_layer.objects.active = age_arm
|
||||
age_arm.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
ebones = age_arm.data.edit_bones
|
||||
|
||||
new_head = {}
|
||||
new_tail = {}
|
||||
order = sorted(ebones, key=_depth)
|
||||
for eb in order:
|
||||
old_h = adult_head[eb.name]
|
||||
old_t = adult_tail[eb.name]
|
||||
direction = old_t - old_h
|
||||
old_len = direction.length
|
||||
if old_len <= 1e-6:
|
||||
new_h = old_h.copy()
|
||||
new_t = old_t.copy()
|
||||
new_head[eb.name] = new_h
|
||||
new_tail[eb.name] = new_t
|
||||
continue
|
||||
direction.normalize()
|
||||
|
||||
region = bone_region(eb.name)
|
||||
lscale = float(profile[region["length"]])
|
||||
rscale = float(profile[region["radial"]])
|
||||
oscale = float(profile[region["offset"]])
|
||||
|
||||
if eb.parent is not None and eb.parent.name in new_head:
|
||||
pname = eb.parent.name
|
||||
if eb.use_connect:
|
||||
new_h = new_tail[pname].copy()
|
||||
else:
|
||||
parent_old_h = adult_head[pname]
|
||||
offset = old_h - parent_old_h
|
||||
new_h = new_head[pname] + offset * oscale
|
||||
else:
|
||||
# Root or a parent-less helper: keep the origin, scale nothing
|
||||
# laterally here (the profile handles the hips length).
|
||||
new_h = old_h.copy()
|
||||
|
||||
new_len = old_len * lscale
|
||||
new_t = new_h + direction * new_len
|
||||
|
||||
eb.head = new_h
|
||||
eb.tail = new_t
|
||||
# roll is intentionally untouched
|
||||
new_head[eb.name] = new_h.copy()
|
||||
new_tail[eb.name] = new_t.copy()
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
age_arm.select_set(False)
|
||||
return age_arm
|
||||
|
||||
|
||||
def _bone_scale_matrix(lscale: float, rscale: float) -> Matrix:
|
||||
return Matrix.Diagonal((rscale, lscale, rscale, 1.0))
|
||||
|
||||
|
||||
def rest_transform_data(adult_arm, age_arm, age: str,
|
||||
profile: dict | None = None) -> dict:
|
||||
"""Build serializable rest-pose deformation data for every bone."""
|
||||
age = profiles.normalize_age(age)
|
||||
profile = profile or profiles.profile(age)
|
||||
out = {}
|
||||
for ab in adult_arm.data.bones:
|
||||
bb = age_arm.data.bones.get(ab.name)
|
||||
if bb is None:
|
||||
continue
|
||||
region = bone_region(ab.name)
|
||||
lscale = float(profile[region["length"]])
|
||||
rscale = float(profile[region["radial"]])
|
||||
f_adult = ab.matrix_local.copy()
|
||||
f_age = bb.matrix_local.copy()
|
||||
s = _bone_scale_matrix(lscale, rscale)
|
||||
d = f_age @ s @ f_adult.inverted()
|
||||
out[ab.name] = {
|
||||
"lengthScale": lscale,
|
||||
"radialScale": rscale,
|
||||
"fAdult": [list(row) for row in f_adult],
|
||||
"fAge": [list(row) for row in f_age],
|
||||
"dMatrix": [list(row) for row in d],
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def dmatrices_from_data(data: dict) -> dict:
|
||||
return {name: Matrix(entry["dMatrix"]) for name, entry in data.items()}
|
||||
|
||||
|
||||
def bone_length_ratios(adult_arm, age_arm) -> dict:
|
||||
out = {}
|
||||
for ab in adult_arm.data.bones:
|
||||
bb = age_arm.data.bones.get(ab.name)
|
||||
if bb is None:
|
||||
continue
|
||||
al = (ab.tail_local - ab.head_local).length
|
||||
bl = (bb.tail_local - bb.head_local).length
|
||||
out[ab.name] = (bl / al) if al > 1e-9 else 1.0
|
||||
return out
|
||||
|
||||
|
||||
def validate_rest_orientation(adult_arm, age_arm, tol=1e-4) -> list:
|
||||
"""Return a list of bones whose normalized rest direction changed."""
|
||||
bad = []
|
||||
for ab in adult_arm.data.bones:
|
||||
bb = age_arm.data.bones.get(ab.name)
|
||||
if bb is None:
|
||||
continue
|
||||
ad = (ab.tail_local - ab.head_local)
|
||||
bd = (bb.tail_local - bb.head_local)
|
||||
if ad.length < 1e-3 or bd.length < 1e-3:
|
||||
continue
|
||||
ad.normalize()
|
||||
bd.normalize()
|
||||
if (ad - bd).length > tol:
|
||||
bad.append(ab.name)
|
||||
return bad
|
||||
|
||||
|
||||
def head_pivots(adult_arm, age_arm, head_bone="mixamorig:Head"):
|
||||
ab = adult_arm.data.bones.get(head_bone)
|
||||
bb = age_arm.data.bones.get(head_bone)
|
||||
if not ab or not bb:
|
||||
return Vector((0, 0, 0)), Vector((0, 0, 0))
|
||||
return ab.head_local.copy(), bb.head_local.copy()
|
||||
|
||||
|
||||
def scale_hair_armature(hair_arm, adult_head_pivot: Vector,
|
||||
age_head_pivot: Vector, scale: float):
|
||||
"""Scale a hair armature uniformly from the adult head to the age head.
|
||||
|
||||
Hair armatures are rigidly attached to the body head at runtime; a uniform
|
||||
scale around the scalp preserves their own bone chain and animations.
|
||||
"""
|
||||
bpy.context.view_layer.objects.active = hair_arm
|
||||
hair_arm.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
for eb in hair_arm.data.edit_bones:
|
||||
old_h = eb.head.copy()
|
||||
old_t = eb.tail.copy()
|
||||
eb.head = age_head_pivot + (old_h - adult_head_pivot) * scale
|
||||
eb.tail = age_head_pivot + (old_t - adult_head_pivot) * scale
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
hair_arm.select_set(False)
|
||||
|
||||
|
||||
def transform_point_uniform(p: Vector, adult_pivot: Vector,
|
||||
age_pivot: Vector, scale: float) -> Vector:
|
||||
return age_pivot + (p - adult_pivot) * scale
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Vertex-weight utilities for the age converter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def limit_vertex_influences(obj, max_influences=4):
|
||||
"""Keep the strongest N weights per vertex and normalize them to 1."""
|
||||
if obj.type != 'MESH' or not obj.vertex_groups:
|
||||
return 0
|
||||
groups = {g.index: g for g in obj.vertex_groups}
|
||||
mesh = obj.data
|
||||
changed = 0
|
||||
for v in mesh.vertices:
|
||||
ws = [(g.group, float(g.weight)) for g in v.groups
|
||||
if g.weight > 0.0 and g.group in groups]
|
||||
if not ws:
|
||||
continue
|
||||
ws.sort(key=lambda p: p[1], reverse=True)
|
||||
keep = ws[:max_influences]
|
||||
drop = ws[max_influences:]
|
||||
total = sum(w for _, w in keep)
|
||||
if total <= 1e-9:
|
||||
continue
|
||||
for gi, _ in drop:
|
||||
groups[gi].remove([v.index])
|
||||
for gi, w in keep:
|
||||
groups[gi].add([v.index], w / total, 'REPLACE')
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
def count_max_influences(obj):
|
||||
if obj.type != 'MESH':
|
||||
return 0
|
||||
best = 0
|
||||
for v in obj.data.vertices:
|
||||
best = max(best, sum(1 for g in v.groups if g.weight > 0.0))
|
||||
return best
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
nude=@NUDE_STATE@
|
||||
source=@NUDE_SRC@
|
||||
@@ -6,6 +6,11 @@ import json
|
||||
import mathutils
|
||||
from mathutils.bvhtree import BVHTree
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
import age_profiles
|
||||
|
||||
def load_blend_files(clothes_blend_path, body_blend_path):
|
||||
"""Load objects from blend files and return all loaded objects
|
||||
|
||||
@@ -364,6 +369,28 @@ def run_batch_combine():
|
||||
print(f"Found {len(clothing_layer1)} layer 1 clothing objects")
|
||||
print(f"Found {len(clothing_layer2)} layer 2 clothing objects")
|
||||
|
||||
# Guard against mixing ages/sexes in one pipeline run. The CMake inputs
|
||||
# are per age/sex; this catches a misnamed file before it bakes nonsense.
|
||||
ages = {age_profiles.normalize_age(o.get("age", "adult"))
|
||||
for o in body_objects}
|
||||
sexes = {str(o.get("sex", "")) for o in body_objects}
|
||||
if len(ages) != 1 or len(sexes) != 1:
|
||||
raise RuntimeError(
|
||||
f"combine_clothes: mixed body ages/sexes: "
|
||||
f"ages={sorted(ages)} sexes={sorted(sexes)}")
|
||||
target_age = next(iter(ages))
|
||||
target_sex = next(iter(sexes))
|
||||
for o in clothing_layer1 + clothing_layer2:
|
||||
obj_age = age_profiles.normalize_age(o.get("ref_age", "adult"))
|
||||
obj_sex = str(o.get("ref_sex", ""))
|
||||
if obj_age != target_age or obj_sex != target_sex:
|
||||
raise RuntimeError(
|
||||
f"combine_clothes: {o.name} is {obj_age}/{obj_sex}, "
|
||||
f"body is {target_age}/{target_sex}")
|
||||
# Keep the authoritative band on the source garment.
|
||||
o["ref_age"] = target_age
|
||||
o["ref_sex"] = target_sex
|
||||
|
||||
body_objects_dict = {obj.name: obj for obj in body_objects}
|
||||
whitelist = set()
|
||||
combined_objects = []
|
||||
@@ -474,7 +501,11 @@ def run_batch_combine():
|
||||
else:
|
||||
obj["clothing_tags"] = ""
|
||||
|
||||
print(f"Finalized metadata for {obj.name}: layer={obj['layer']}, garments={obj['garments']}, tags={obj['clothing_tags']}")
|
||||
obj["age"] = target_age
|
||||
obj["sex"] = target_sex
|
||||
print(f"Finalized metadata for {obj.name}: age={target_age}, "
|
||||
f"sex={target_sex}, layer={obj['layer']}, "
|
||||
f"garments={obj['garments']}, tags={obj['clothing_tags']}")
|
||||
|
||||
del obj["_combined_meta"]
|
||||
|
||||
|
||||
@@ -7,6 +7,89 @@ _script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if _script_dir not in sys.path:
|
||||
sys.path.insert(0, _script_dir)
|
||||
from transfer_shape_keys import fix_seams_across_objects, fix_normals_across_objects
|
||||
import age_profiles
|
||||
|
||||
|
||||
def _catalog_objects():
|
||||
required = ("age", "sex", "slot")
|
||||
return [o for o in bpy.data.objects
|
||||
if o.type == 'MESH' and all(p in o for p in required)]
|
||||
|
||||
|
||||
def _own_armature_names():
|
||||
names = set()
|
||||
for obj in _catalog_objects():
|
||||
if obj.get("has_own_armature", False):
|
||||
name = obj.get("own_armature_name", "")
|
||||
if name and bpy.data.objects.get(name):
|
||||
names.add(name)
|
||||
return names
|
||||
|
||||
|
||||
def consolidate_armatures():
|
||||
"""Leave exactly one body armature per consolidated blend.
|
||||
|
||||
The clothing stages append their own copy of the body rig; Blender
|
||||
renames those copies to .001/.002/... They are unused and previously
|
||||
leaked into the export as extra skeletons.
|
||||
"""
|
||||
cats = _catalog_objects()
|
||||
if not cats:
|
||||
print("WARNING: no catalog objects; cannot consolidate armatures")
|
||||
return
|
||||
|
||||
ages = {age_profiles.normalize_age(o.get("age", "adult"))
|
||||
for o in cats}
|
||||
sexes = {str(o.get("sex", "male")) for o in cats}
|
||||
if len(ages) != 1 or len(sexes) != 1:
|
||||
raise RuntimeError(
|
||||
f"mixed ages/sexes in one consolidated blend: "
|
||||
f"ages={sorted(ages)} sexes={sorted(sexes)}")
|
||||
age = next(iter(ages))
|
||||
sex = next(iter(sexes))
|
||||
expected = age_profiles.armature_name(age, sex)
|
||||
|
||||
body = bpy.data.objects.get(expected)
|
||||
if body is None or body.type != 'ARMATURE':
|
||||
# Choose the armature with the most catalog children / deform bones.
|
||||
best = None
|
||||
best_score = -1
|
||||
for arm in [o for o in bpy.data.objects if o.type == 'ARMATURE']:
|
||||
children = sum(1 for o in cats if o.parent == arm)
|
||||
score = children * 10000 + len(arm.data.bones)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = arm
|
||||
body = best
|
||||
if body is None:
|
||||
raise RuntimeError("no body armature found in consolidated blend")
|
||||
|
||||
body.name = expected
|
||||
body.data.name = expected
|
||||
own = _own_armature_names()
|
||||
|
||||
for obj in cats:
|
||||
if obj.get("has_own_armature", False):
|
||||
continue
|
||||
obj.parent = body
|
||||
arm_mod = next((m for m in obj.modifiers
|
||||
if m.type == 'ARMATURE'), None)
|
||||
if arm_mod is None:
|
||||
arm_mod = obj.modifiers.new(name="Armature", type='ARMATURE')
|
||||
arm_mod.object = body
|
||||
|
||||
removed = []
|
||||
for arm in [o for o in bpy.data.objects if o.type == 'ARMATURE']:
|
||||
if arm.name == body.name or arm.name in own:
|
||||
continue
|
||||
removed.append(arm.name)
|
||||
bpy.data.objects.remove(arm, do_unlink=True)
|
||||
if removed:
|
||||
print(f"Removed duplicate body armatures: {removed}")
|
||||
print(f"Consolidated body armature: '{body.name}' "
|
||||
f"({len(body.data.bones)} bones), "
|
||||
f"hair armatures kept: {sorted(own)}")
|
||||
|
||||
|
||||
def process_append(source_files, output_path):
|
||||
required_props = {"age", "sex", "slot"}
|
||||
@@ -92,7 +175,8 @@ def process_append(source_files, output_path):
|
||||
continue
|
||||
|
||||
# Normal body part: parent to body armature
|
||||
arm_name = obj.get("sex")
|
||||
arm_name = age_profiles.armature_name(
|
||||
obj.get("age", "adult"), obj.get("sex", "male"))
|
||||
arm_obj = bpy.data.objects.get(arm_name)
|
||||
|
||||
if arm_obj and arm_obj.type == 'ARMATURE':
|
||||
@@ -121,10 +205,14 @@ def process_append(source_files, output_path):
|
||||
else:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# 4. Recursive Purge of all unlinked data (Materials, Textures, Meshes)
|
||||
# 4. Hardening: one body armature, no duplicate clothing-stage copies.
|
||||
print("\nConsolidating body armatures...")
|
||||
consolidate_armatures()
|
||||
|
||||
# 5. Recursive Purge of all unlinked data (Materials, Textures, Meshes)
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
# 5. Fix seams across ALL body parts in the consolidated scene.
|
||||
# 6. Fix seams across ALL body parts in the consolidated scene.
|
||||
print("\nFixing seams across all consolidated body parts...")
|
||||
fix_seams_across_objects()
|
||||
fix_normals_across_objects()
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
{
|
||||
"age": "baby",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "female",
|
||||
"ageArmature": "baby-female",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7028,
|
||||
"stem": "baby-female"
|
||||
},
|
||||
"outputBase": "edited-normal-baby-female.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-female.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7372296166046396,
|
||||
"edgeRatioMin": 0.3150672796769405,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 40,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 69
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.153788134245814,
|
||||
"edgeRatioMin": 0.0842179761594005,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6227,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6578
|
||||
},
|
||||
"Body-merged": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.153788134245814,
|
||||
"edgeRatioMin": 0.07910523007157484,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 8423,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9178
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5943739947353248,
|
||||
"edgeRatioMin": 0.24871503022916608,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1242,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1194
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.6102128302187958,
|
||||
"edgeRatioMin": 0.23948013201029564,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1280,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1050
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.153788134245814,
|
||||
"edgeRatioMin": 0.0842179761594005,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3701,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 3354
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.153788134245814,
|
||||
"edgeRatioMin": 0.07910523007157484,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4727,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 4383
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.153788134245814,
|
||||
"edgeRatioMin": 0.0842179761594005,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6223,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5550
|
||||
},
|
||||
"Cylinder": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 102.90996675755198,
|
||||
"edgeRatioMin": 0.08829058454162445,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 595,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 840,
|
||||
"vertices": 840
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8507524157231209,
|
||||
"edgeRatioMin": 0.8497737875813667,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3887,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4109
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500001569254199,
|
||||
"edgeRatioMin": 0.8499997162406219,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8
|
||||
},
|
||||
"Hair001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8653125183616986,
|
||||
"edgeRatioMin": 0.8197160422662443,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8653125183616986,
|
||||
"edgeRatioMin": 0.8197160422662443,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.4644834678498582,
|
||||
"edgeRatioMin": 0.6099711906323935,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 726,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"OnepieceCloth": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5492324553724546,
|
||||
"edgeRatioMin": 0.5290424031153619,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5911305932486909,
|
||||
"edgeRatioMin": 0.2363519411318045,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 402,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 608
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7959426635498152,
|
||||
"edgeRatioMin": 0.07910523007157484,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1026,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1029
|
||||
},
|
||||
"bra1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.850017759730551,
|
||||
"edgeRatioMin": 0.8499810981151088,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 430,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 419
|
||||
},
|
||||
"bra2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500176808030321,
|
||||
"edgeRatioMin": 0.8499719662092197,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 506,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 524
|
||||
},
|
||||
"bra3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500183886242741,
|
||||
"edgeRatioMin": 0.8499869218274825,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"bra4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500183886242741,
|
||||
"edgeRatioMin": 0.8499869218274825,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"panties": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 389.90044750135564,
|
||||
"edgeRatioMin": 0.1344951283772418,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 674,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 797,
|
||||
"vertices": 797
|
||||
},
|
||||
"panties-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 9.037905091399436,
|
||||
"edgeRatioMin": 0.09492642747770344,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 337,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 425,
|
||||
"vertices": 425
|
||||
},
|
||||
"panties2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 73.44866646636162,
|
||||
"edgeRatioMin": 0.039480502681632265,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 1266,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 1328,
|
||||
"vertices": 1328
|
||||
},
|
||||
"panties2-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 69.03029060930521,
|
||||
"edgeRatioMin": 0.12109997869211511,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 507,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 558,
|
||||
"vertices": 558
|
||||
},
|
||||
"panties3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 56.096226750526704,
|
||||
"edgeRatioMin": 0.07317455801311457,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 758,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 820,
|
||||
"vertices": 820
|
||||
},
|
||||
"panties4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 59.134504340321854,
|
||||
"edgeRatioMin": 0.07317455801311457,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 690,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 752,
|
||||
"vertices": 752
|
||||
},
|
||||
"panties5": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500240796533016,
|
||||
"edgeRatioMin": 0.8499814844536061,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties6": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500154180994606,
|
||||
"edgeRatioMin": 0.8499882205989492,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties7": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500126347595567,
|
||||
"edgeRatioMin": 0.849978243664894,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5538312559806804,
|
||||
"edgeRatioMin": 0.26002334716847597,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 414,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 414
|
||||
},
|
||||
"skirt1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5322821714343262,
|
||||
"edgeRatioMin": 0.27078238670490873,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"skirt2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5392819893864378,
|
||||
"edgeRatioMin": 0.2767377972392573,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 154,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 176
|
||||
},
|
||||
"skirt3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5319295543088763,
|
||||
"edgeRatioMin": 0.2734056437018691,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"top1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500186043293043,
|
||||
"edgeRatioMin": 0.8499912680582199,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 860,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 852
|
||||
},
|
||||
"top2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500086443486258,
|
||||
"edgeRatioMin": 0.8499937873756263,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
},
|
||||
"top3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500086443486258,
|
||||
"edgeRatioMin": 0.8499937873756263,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 252,
|
||||
"ageBones": 252,
|
||||
"profile": {
|
||||
"arm": 0.3,
|
||||
"bodyRadial": 0.55,
|
||||
"foot": 0.55,
|
||||
"head": 0.85,
|
||||
"headBulge": 1.08,
|
||||
"hipHeight": 0.28,
|
||||
"hipWidth": 0.55,
|
||||
"leg": 0.26,
|
||||
"limbRadial": 0.5,
|
||||
"shoulderWidth": 0.5,
|
||||
"torso": 0.3,
|
||||
"walkSpeedScale": 0.0
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 3697,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0012,
|
||||
-0.0342,
|
||||
1.5629
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.001,
|
||||
-0.0343,
|
||||
1.5628
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 5.9825688860243066e-06,
|
||||
"snappedVertices": 95
|
||||
},
|
||||
"sex": "female",
|
||||
"status": "warn",
|
||||
"stem": "baby-female",
|
||||
"timestamp": 1793419311,
|
||||
"warnings": [
|
||||
"Accessory",
|
||||
"Body",
|
||||
"Body-merged",
|
||||
"Body_shapes",
|
||||
"BodyBottom",
|
||||
"BodyFeet",
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Shoes",
|
||||
"Tops",
|
||||
"BodyTop",
|
||||
"BodyBottom",
|
||||
"Cylinder",
|
||||
"panties",
|
||||
"panties-full",
|
||||
"panties2",
|
||||
"panties2-full",
|
||||
"panties3",
|
||||
"panties4",
|
||||
"skirt1",
|
||||
"skirt2",
|
||||
"skirt3",
|
||||
"BodyFeet",
|
||||
"shoes1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Conversion Report — baby / female
|
||||
|
||||
- Stem: `baby-female`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 37
|
||||
- Warnings: 24
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `female`
|
||||
- ageArmature: `baby-female`
|
||||
- animations: `{'scaledLocationCurves': 7028, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'baby-female'}`
|
||||
- outputBase: `edited-normal-baby-female.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-female.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `3697`
|
||||
- snappedVertices: `95`
|
||||
- maxGap: `5.9825688860243066e-06`
|
||||
- examples: `[{'position': [0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0012, -0.0342, 1.5629], 'gap': 3e-06, 'objects': ['Hair2']}, {'position': [-0.001, -0.0343, 1.5628], 'gap': 3e-06, 'objects': ['Hair2']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `Accessory`
|
||||
- `Body`
|
||||
- `Body-merged`
|
||||
- `Body_shapes`
|
||||
- `BodyBottom`
|
||||
- `BodyFeet`
|
||||
- `BodyTop`
|
||||
- `BodyTopRobe`
|
||||
- `Shoes`
|
||||
- `Tops`
|
||||
- `BodyTop`
|
||||
- `BodyBottom`
|
||||
- `Cylinder`
|
||||
- `panties`
|
||||
- `panties-full`
|
||||
- `panties2`
|
||||
- `panties2-full`
|
||||
- `panties3`
|
||||
- `panties4`
|
||||
- `skirt1`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — warn; verts=69, tris=0 degenerate, influences=4, unweighted=0, edge=0.32..0.74
|
||||
- `Body` — warn; verts=6578, tris=0 degenerate, influences=4, unweighted=0, edge=0.08..2.15
|
||||
- `Body-merged` — warn; verts=9178, tris=0 degenerate, influences=4, unweighted=0, edge=0.08..2.15
|
||||
- `BodyBottom` — warn; verts=1194, tris=0 degenerate, influences=4, unweighted=0, edge=0.25..0.59
|
||||
- `BodyFeet` — warn; verts=1050, tris=0 degenerate, influences=3, unweighted=0, edge=0.24..0.61
|
||||
- `BodyTop` — warn; verts=3354, tris=0 degenerate, influences=4, unweighted=0, edge=0.08..2.15
|
||||
- `BodyTopRobe` — warn; verts=4383, tris=0 degenerate, influences=4, unweighted=0, edge=0.08..2.15
|
||||
- `Body_shapes` — warn; verts=5550, tris=0 degenerate, influences=4, unweighted=0, edge=0.08..2.15
|
||||
- `Cylinder` — warn; verts=840, tris=0 degenerate, influences=0, unweighted=840, edge=0.09..102.91
|
||||
- `Face` — ok; verts=4109, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `Hair` — ok; verts=8, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `Hair001` — ok; verts=8673, tris=0 degenerate, influences=1, unweighted=0, edge=0.82..0.87
|
||||
- `Hair2` — ok; verts=8673, tris=0 degenerate, influences=2, unweighted=0, edge=0.82..0.87
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=0.61..1.46
|
||||
- `OnepieceCloth` — ok; verts=6, tris=0 degenerate, influences=3, unweighted=0, edge=0.53..0.55
|
||||
- `Shoes` — warn; verts=608, tris=0 degenerate, influences=3, unweighted=0, edge=0.24..0.59
|
||||
- `Tops` — warn; verts=1029, tris=0 degenerate, influences=4, unweighted=0, edge=0.08..0.80
|
||||
- `bra1` — ok; verts=419, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `bra2` — ok; verts=524, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `bra3` — ok; verts=450, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `bra4` — ok; verts=450, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `panties` — warn; verts=797, tris=0 degenerate, influences=0, unweighted=797, edge=0.13..389.90
|
||||
- `panties-full` — warn; verts=425, tris=0 degenerate, influences=0, unweighted=425, edge=0.09..9.04
|
||||
- `panties2` — warn; verts=1328, tris=0 degenerate, influences=0, unweighted=1328, edge=0.04..73.45
|
||||
- `panties2-full` — warn; verts=558, tris=0 degenerate, influences=0, unweighted=558, edge=0.12..69.03
|
||||
- `panties3` — warn; verts=820, tris=0 degenerate, influences=0, unweighted=820, edge=0.07..56.10
|
||||
- `panties4` — warn; verts=752, tris=0 degenerate, influences=0, unweighted=752, edge=0.07..59.13
|
||||
- `panties5` — ok; verts=758, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `panties6` — ok; verts=758, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `panties7` — ok; verts=758, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `shoes1` — warn; verts=414, tris=0 degenerate, influences=3, unweighted=0, edge=0.26..1.55
|
||||
- `skirt1` — warn; verts=154, tris=0 degenerate, influences=3, unweighted=0, edge=0.27..0.53
|
||||
- `skirt2` — warn; verts=176, tris=0 degenerate, influences=3, unweighted=0, edge=0.28..0.54
|
||||
- `skirt3` — warn; verts=154, tris=0 degenerate, influences=3, unweighted=0, edge=0.27..0.53
|
||||
- `top1` — ok; verts=852, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `top2` — ok; verts=854, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `top3` — ok; verts=854, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"age": "baby",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "male",
|
||||
"ageArmature": "baby-male",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014",
|
||||
"Armature|mixamo.com|Layer0.012"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7601,
|
||||
"stem": "baby-male"
|
||||
},
|
||||
"outputBase": "edited-normal-baby-male.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-male.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.6591322275489071,
|
||||
"edgeRatioMin": 0.2294774279830071,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 111,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 145
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.7944548668408027,
|
||||
"edgeRatioMin": 0.09792195397903704,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6232,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 6584
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.696915448832056,
|
||||
"edgeRatioMin": 0.2475168552675534,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1228,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1181
|
||||
},
|
||||
"BodyBottomPants": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8123872063883257,
|
||||
"edgeRatioMin": 0.2475168552675534,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 972,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1133
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.6102393851343675,
|
||||
"edgeRatioMin": 0.22574066520587047,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1274,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1392
|
||||
},
|
||||
"BodyFeetPants": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 0.6102393851343675,
|
||||
"edgeRatioMin": 0.2095881183386803,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1466,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1602
|
||||
},
|
||||
"BodyFeetPantsShoes": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 0.6017141327128214,
|
||||
"edgeRatioMin": 0.2095881183386803,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 830,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1048
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.7944548668408027,
|
||||
"edgeRatioMin": 0.09792195397903704,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3726,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 3373
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.694739629684512,
|
||||
"edgeRatioMin": 0.04884643632625867,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 5384,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 5046
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.7944548668408027,
|
||||
"edgeRatioMin": 0.09792195397903704,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6228,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5556
|
||||
},
|
||||
"Bottoms": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8123872063883257,
|
||||
"edgeRatioMin": 0.2095881183386803,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 826,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 942
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8501110244008585,
|
||||
"edgeRatioMin": 0.8499080602231018,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3839,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4045
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8500001087034722,
|
||||
"edgeRatioMin": 0.8499998996166309,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 4,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8932439134046963,
|
||||
"edgeRatioMin": 0.8193953530826958,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.4800221295791642,
|
||||
"edgeRatioMin": 0.6036557832095256,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 724,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"Panties001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.6962243553417347,
|
||||
"edgeRatioMin": 0.2658767663648385,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 604,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 582
|
||||
},
|
||||
"Pants0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.651774029445214,
|
||||
"edgeRatioMin": 0.2253732629144712,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 898,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 915
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.6017141327128214,
|
||||
"edgeRatioMin": 0.39686482033435044,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.6309442432828267,
|
||||
"edgeRatioMin": 0.04884643632625867,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"TopsEdited": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.6309442432828267,
|
||||
"edgeRatioMin": 0.04884643632625867,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"coat": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5376462038278786,
|
||||
"edgeRatioMin": 0.14071060392477913,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"hair0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.322546280419375,
|
||||
"edgeRatioMin": 0.6481636259388702,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0002": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 2.322546280419375,
|
||||
"edgeRatioMin": 0.6481636259388702,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0003": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"edgeRatioMin": 1.0,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.5819192631554228,
|
||||
"edgeRatioMin": 0.36626946181073133,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 242,
|
||||
"ageBones": 242,
|
||||
"profile": {
|
||||
"arm": 0.3,
|
||||
"bodyRadial": 0.55,
|
||||
"foot": 0.55,
|
||||
"head": 0.85,
|
||||
"headBulge": 1.08,
|
||||
"hipHeight": 0.28,
|
||||
"hipWidth": 0.55,
|
||||
"leg": 0.26,
|
||||
"limbRadial": 0.5,
|
||||
"shoulderWidth": 0.5,
|
||||
"torso": 0.3,
|
||||
"walkSpeedScale": 0.0
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 5119,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
-0.0184,
|
||||
1.5008
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0636,
|
||||
0.0319,
|
||||
1.6457
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0698,
|
||||
0.021,
|
||||
1.649
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0716,
|
||||
0.0206,
|
||||
1.6529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0636,
|
||||
0.0319,
|
||||
1.6457
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0698,
|
||||
0.021,
|
||||
1.649
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0716,
|
||||
0.0206,
|
||||
1.6529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
0.0002,
|
||||
-0.0352,
|
||||
1.8214
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 6.834857277971055e-06,
|
||||
"snappedVertices": 204
|
||||
},
|
||||
"sex": "male",
|
||||
"status": "warn",
|
||||
"stem": "baby-male",
|
||||
"timestamp": 1793419306,
|
||||
"warnings": [
|
||||
"Accessory",
|
||||
"Body",
|
||||
"Body_shapes",
|
||||
"BodyBottom",
|
||||
"BodyBottomPants",
|
||||
"BodyFeet",
|
||||
"BodyFeetPants",
|
||||
"BodyFeetPantsShoes",
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Bottoms",
|
||||
"Tops",
|
||||
"TopsEdited",
|
||||
"coat",
|
||||
"Panties001",
|
||||
"Pants0001",
|
||||
"BodyFeet"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# Conversion Report — baby / male
|
||||
|
||||
- Stem: `baby-male`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 25
|
||||
- Warnings: 17
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `male`
|
||||
- ageArmature: `baby-male`
|
||||
- animations: `{'scaledLocationCurves': 7601, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014', 'Armature|mixamo.com|Layer0.012'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'baby-male'}`
|
||||
- outputBase: `edited-normal-baby-male.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-male.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `5119`
|
||||
- snappedVertices: `204`
|
||||
- maxGap: `6.834857277971055e-06`
|
||||
- examples: `[{'position': [0.0, -0.0184, 1.5008], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0633, 0.0369, 1.6496], 'gap': 1e-06, 'objects': ['BodyTop', 'Face']}, {'position': [-0.0633, 0.0369, 1.6496], 'gap': 1e-06, 'objects': ['BodyTop', 'Face']}, {'position': [0.0636, 0.0319, 1.6457], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0698, 0.021, 1.649], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0716, 0.0206, 1.6529], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0636, 0.0319, 1.6457], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0698, 0.021, 1.649], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0716, 0.0206, 1.6529], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0002, -0.0352, 1.8214], 'gap': 2e-06, 'objects': ['Hair2']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `Accessory`
|
||||
- `Body`
|
||||
- `Body_shapes`
|
||||
- `BodyBottom`
|
||||
- `BodyBottomPants`
|
||||
- `BodyFeet`
|
||||
- `BodyFeetPants`
|
||||
- `BodyFeetPantsShoes`
|
||||
- `BodyTop`
|
||||
- `BodyTopRobe`
|
||||
- `Bottoms`
|
||||
- `Tops`
|
||||
- `TopsEdited`
|
||||
- `coat`
|
||||
- `Panties001`
|
||||
- `Pants0001`
|
||||
- `BodyFeet`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — warn; verts=145, tris=0 degenerate, influences=4, unweighted=0, edge=0.23..0.66
|
||||
- `Body` — warn; verts=6584, tris=0 degenerate, influences=4, unweighted=0, edge=0.10..2.79
|
||||
- `BodyBottom` — warn; verts=1181, tris=0 degenerate, influences=4, unweighted=0, edge=0.25..0.70
|
||||
- `BodyBottomPants` — warn; verts=1133, tris=0 degenerate, influences=4, unweighted=0, edge=0.25..0.81
|
||||
- `BodyFeet` — warn; verts=1392, tris=0 degenerate, influences=3, unweighted=0, edge=0.23..0.61
|
||||
- `BodyFeetPants` — warn; verts=1602, tris=48 degenerate, influences=3, unweighted=0, edge=0.21..0.61
|
||||
- `BodyFeetPantsShoes` — warn; verts=1048, tris=48 degenerate, influences=3, unweighted=0, edge=0.21..0.60
|
||||
- `BodyTop` — warn; verts=3373, tris=0 degenerate, influences=4, unweighted=0, edge=0.10..2.79
|
||||
- `BodyTopRobe` — warn; verts=5046, tris=0 degenerate, influences=4, unweighted=0, edge=0.05..2.69
|
||||
- `Body_shapes` — warn; verts=5556, tris=0 degenerate, influences=4, unweighted=0, edge=0.10..2.79
|
||||
- `Bottoms` — warn; verts=942, tris=0 degenerate, influences=4, unweighted=0, edge=0.21..0.81
|
||||
- `Face` — ok; verts=4045, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `Hair` — ok; verts=9, tris=0 degenerate, influences=1, unweighted=0, edge=0.85..0.85
|
||||
- `Hair2` — ok; verts=7022, tris=0 degenerate, influences=2, unweighted=0, edge=0.82..0.89
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=0.60..1.48
|
||||
- `Panties001` — warn; verts=582, tris=0 degenerate, influences=4, unweighted=0, edge=0.27..0.70
|
||||
- `Pants0001` — warn; verts=915, tris=0 degenerate, influences=4, unweighted=0, edge=0.23..1.65
|
||||
- `Shoes` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=0.40..0.60
|
||||
- `Tops` — warn; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.05..1.63
|
||||
- `TopsEdited` — warn; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.05..1.63
|
||||
- `coat` — warn; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.14..1.54
|
||||
- `hair0001` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=0.65..2.32
|
||||
- `hair0002` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=0.65..2.32
|
||||
- `hair0003` — ok; verts=7022, tris=0 degenerate, influences=0, unweighted=0, edge=1.00..1.00
|
||||
- `shoes1` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=0.37..0.58
|
||||
@@ -0,0 +1,627 @@
|
||||
{
|
||||
"age": "child",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "female",
|
||||
"ageArmature": "child-female",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7028,
|
||||
"stem": "child-female"
|
||||
},
|
||||
"outputBase": "edited-normal-child-female.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-female.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7898433968486418,
|
||||
"edgeRatioMin": 0.49563877021449726,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 40,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 69
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5936254360808924,
|
||||
"edgeRatioMin": 0.35974842621764863,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6227,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6578
|
||||
},
|
||||
"Body-merged": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5936254360808924,
|
||||
"edgeRatioMin": 0.35974842621764863,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 8423,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9178
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7850192237493911,
|
||||
"edgeRatioMin": 0.5419409362099876,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1242,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1194
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7839441718995033,
|
||||
"edgeRatioMin": 0.5345308894982719,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1280,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1050
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5936254360808924,
|
||||
"edgeRatioMin": 0.35974842621764863,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3701,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 3354
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5936254360808924,
|
||||
"edgeRatioMin": 0.35974842621764863,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4727,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 4383
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.5936254360808924,
|
||||
"edgeRatioMin": 0.35974842621764863,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6223,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5550
|
||||
},
|
||||
"Cylinder": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 55.52669888099699,
|
||||
"edgeRatioMin": 0.3728675302592677,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 595,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 840,
|
||||
"vertices": 840
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9209191562868269,
|
||||
"edgeRatioMin": 0.9194440137740598,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3887,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4109
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9200007036314336,
|
||||
"edgeRatioMin": 0.9199996884063618,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8
|
||||
},
|
||||
"Hair001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9580249853514248,
|
||||
"edgeRatioMin": 0.8921870820834138,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9580249853514248,
|
||||
"edgeRatioMin": 0.8921870820834138,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.243301152005224,
|
||||
"edgeRatioMin": 0.7996069035600766,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 726,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"OnepieceCloth": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7795104096985646,
|
||||
"edgeRatioMin": 0.7657916805350437,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7710505397361299,
|
||||
"edgeRatioMin": 0.5274789367897975,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 402,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 608
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8665602233865596,
|
||||
"edgeRatioMin": 0.44064144963083524,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1026,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1029
|
||||
},
|
||||
"bra1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.920044969946938,
|
||||
"edgeRatioMin": 0.9199654645832204,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 430,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 419
|
||||
},
|
||||
"bra2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9200462644396492,
|
||||
"edgeRatioMin": 0.9199754607547745,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 506,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 524
|
||||
},
|
||||
"bra3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9200246096786325,
|
||||
"edgeRatioMin": 0.9199754607547745,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"bra4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9200246096786325,
|
||||
"edgeRatioMin": 0.9199754607547745,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"panties": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 211.10052027794262,
|
||||
"edgeRatioMin": 0.19951643125297738,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 674,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 797,
|
||||
"vertices": 797
|
||||
},
|
||||
"panties-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.322137784611612,
|
||||
"edgeRatioMin": 0.21340035981963004,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 337,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 425,
|
||||
"vertices": 425
|
||||
},
|
||||
"panties2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 39.470945630173105,
|
||||
"edgeRatioMin": 0.21985285672118582,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 1266,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 1328,
|
||||
"vertices": 1328
|
||||
},
|
||||
"panties2-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 36.99110437234406,
|
||||
"edgeRatioMin": 0.10284348688443892,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 507,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 558,
|
||||
"vertices": 558
|
||||
},
|
||||
"panties3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 30.204732645010235,
|
||||
"edgeRatioMin": 0.20627087124391974,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 758,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 820,
|
||||
"vertices": 820
|
||||
},
|
||||
"panties4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 31.683176789997283,
|
||||
"edgeRatioMin": 0.1989474077553321,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 690,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 752,
|
||||
"vertices": 752
|
||||
},
|
||||
"panties5": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 5.895118610080954,
|
||||
"edgeRatioMin": 0.6048716072518844,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties6": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.880647390006613,
|
||||
"edgeRatioMin": 0.6250818099712813,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties7": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.880647390006613,
|
||||
"edgeRatioMin": 0.6400929469228386,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9595848242017094,
|
||||
"edgeRatioMin": 0.5492882605967488,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 414,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 414
|
||||
},
|
||||
"skirt1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.3290984484841972,
|
||||
"edgeRatioMin": 0.3753802859501542,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"skirt2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.4244777332130143,
|
||||
"edgeRatioMin": 0.3753802859501542,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 154,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 176
|
||||
},
|
||||
"skirt3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.268563086876801,
|
||||
"edgeRatioMin": 0.37917544169158424,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"top1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.0781828943354546,
|
||||
"edgeRatioMin": 0.7089442731001407,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 860,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 852
|
||||
},
|
||||
"top2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.0781828943354546,
|
||||
"edgeRatioMin": 0.7089442731001407,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
},
|
||||
"top3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.0781828943354546,
|
||||
"edgeRatioMin": 0.7089442731001407,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 252,
|
||||
"ageBones": 252,
|
||||
"profile": {
|
||||
"arm": 0.6,
|
||||
"bodyRadial": 0.78,
|
||||
"foot": 0.75,
|
||||
"head": 0.92,
|
||||
"headBulge": 1.03,
|
||||
"hipHeight": 0.55,
|
||||
"hipWidth": 0.72,
|
||||
"leg": 0.55,
|
||||
"limbRadial": 0.72,
|
||||
"shoulderWidth": 0.7,
|
||||
"torso": 0.62,
|
||||
"walkSpeedScale": 0.75
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 3697,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0012,
|
||||
-0.0342,
|
||||
1.5629
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.001,
|
||||
-0.0343,
|
||||
1.5628
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 6.470887154236068e-06,
|
||||
"snappedVertices": 95
|
||||
},
|
||||
"sex": "female",
|
||||
"status": "warn",
|
||||
"stem": "child-female",
|
||||
"timestamp": 1793419297,
|
||||
"warnings": [
|
||||
"top1",
|
||||
"top2",
|
||||
"top3",
|
||||
"Cylinder",
|
||||
"panties",
|
||||
"panties-full",
|
||||
"panties2",
|
||||
"panties2-full",
|
||||
"panties3",
|
||||
"panties4",
|
||||
"panties5",
|
||||
"panties6",
|
||||
"panties7"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# Conversion Report — child / female
|
||||
|
||||
- Stem: `child-female`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 37
|
||||
- Warnings: 13
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `female`
|
||||
- ageArmature: `child-female`
|
||||
- animations: `{'scaledLocationCurves': 7028, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'child-female'}`
|
||||
- outputBase: `edited-normal-child-female.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-female.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `3697`
|
||||
- snappedVertices: `95`
|
||||
- maxGap: `6.470887154236068e-06`
|
||||
- examples: `[{'position': [0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0012, -0.0342, 1.5629], 'gap': 3e-06, 'objects': ['Hair2']}, {'position': [-0.001, -0.0343, 1.5628], 'gap': 3e-06, 'objects': ['Hair2']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `top1`
|
||||
- `top2`
|
||||
- `top3`
|
||||
- `Cylinder`
|
||||
- `panties`
|
||||
- `panties-full`
|
||||
- `panties2`
|
||||
- `panties2-full`
|
||||
- `panties3`
|
||||
- `panties4`
|
||||
- `panties5`
|
||||
- `panties6`
|
||||
- `panties7`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — ok; verts=69, tris=0 degenerate, influences=4, unweighted=0, edge=0.50..0.79
|
||||
- `Body` — ok; verts=6578, tris=0 degenerate, influences=4, unweighted=0, edge=0.36..1.59
|
||||
- `Body-merged` — ok; verts=9178, tris=0 degenerate, influences=4, unweighted=0, edge=0.36..1.59
|
||||
- `BodyBottom` — ok; verts=1194, tris=0 degenerate, influences=4, unweighted=0, edge=0.54..0.79
|
||||
- `BodyFeet` — ok; verts=1050, tris=0 degenerate, influences=3, unweighted=0, edge=0.53..0.78
|
||||
- `BodyTop` — ok; verts=3354, tris=0 degenerate, influences=4, unweighted=0, edge=0.36..1.59
|
||||
- `BodyTopRobe` — ok; verts=4383, tris=0 degenerate, influences=4, unweighted=0, edge=0.36..1.59
|
||||
- `Body_shapes` — ok; verts=5550, tris=0 degenerate, influences=4, unweighted=0, edge=0.36..1.59
|
||||
- `Cylinder` — warn; verts=840, tris=0 degenerate, influences=0, unweighted=840, edge=0.37..55.53
|
||||
- `Face` — ok; verts=4109, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `Hair` — ok; verts=8, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `Hair001` — ok; verts=8673, tris=0 degenerate, influences=1, unweighted=0, edge=0.89..0.96
|
||||
- `Hair2` — ok; verts=8673, tris=0 degenerate, influences=2, unweighted=0, edge=0.89..0.96
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=0.80..1.24
|
||||
- `OnepieceCloth` — ok; verts=6, tris=0 degenerate, influences=3, unweighted=0, edge=0.77..0.78
|
||||
- `Shoes` — ok; verts=608, tris=0 degenerate, influences=3, unweighted=0, edge=0.53..0.77
|
||||
- `Tops` — ok; verts=1029, tris=0 degenerate, influences=4, unweighted=0, edge=0.44..0.87
|
||||
- `bra1` — ok; verts=419, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `bra2` — ok; verts=524, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `bra3` — ok; verts=450, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `bra4` — ok; verts=450, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `panties` — warn; verts=797, tris=0 degenerate, influences=0, unweighted=797, edge=0.20..211.10
|
||||
- `panties-full` — warn; verts=425, tris=0 degenerate, influences=0, unweighted=425, edge=0.21..7.32
|
||||
- `panties2` — warn; verts=1328, tris=0 degenerate, influences=0, unweighted=1328, edge=0.22..39.47
|
||||
- `panties2-full` — warn; verts=558, tris=0 degenerate, influences=0, unweighted=558, edge=0.10..36.99
|
||||
- `panties3` — warn; verts=820, tris=0 degenerate, influences=0, unweighted=820, edge=0.21..30.20
|
||||
- `panties4` — warn; verts=752, tris=0 degenerate, influences=0, unweighted=752, edge=0.20..31.68
|
||||
- `panties5` — warn; verts=758, tris=0 degenerate, influences=3, unweighted=0, edge=0.60..5.90
|
||||
- `panties6` — warn; verts=758, tris=0 degenerate, influences=3, unweighted=0, edge=0.63..7.88
|
||||
- `panties7` — warn; verts=758, tris=0 degenerate, influences=3, unweighted=0, edge=0.64..7.88
|
||||
- `shoes1` — ok; verts=414, tris=0 degenerate, influences=3, unweighted=0, edge=0.55..0.96
|
||||
- `skirt1` — ok; verts=154, tris=0 degenerate, influences=4, unweighted=0, edge=0.38..1.33
|
||||
- `skirt2` — ok; verts=176, tris=0 degenerate, influences=4, unweighted=0, edge=0.38..1.42
|
||||
- `skirt3` — ok; verts=154, tris=0 degenerate, influences=4, unweighted=0, edge=0.38..1.27
|
||||
- `top1` — warn; verts=852, tris=0 degenerate, influences=1, unweighted=0, edge=0.71..7.08
|
||||
- `top2` — warn; verts=854, tris=0 degenerate, influences=1, unweighted=0, edge=0.71..7.08
|
||||
- `top3` — warn; verts=854, tris=0 degenerate, influences=1, unweighted=0, edge=0.71..7.08
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"age": "child",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "male",
|
||||
"ageArmature": "child-male",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014",
|
||||
"Armature|mixamo.com|Layer0.012"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7601,
|
||||
"stem": "child-male"
|
||||
},
|
||||
"outputBase": "edited-normal-child-male.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-male.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7935740735522747,
|
||||
"edgeRatioMin": 0.478987783805106,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 111,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 145
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.9536679047270835,
|
||||
"edgeRatioMin": 0.33440855736119546,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6232,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 6584
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8216992678030084,
|
||||
"edgeRatioMin": 0.5410301282339429,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1228,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1181
|
||||
},
|
||||
"BodyBottomPants": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.86670117423969,
|
||||
"edgeRatioMin": 0.5410301282339429,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 972,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1133
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7839642922675355,
|
||||
"edgeRatioMin": 0.5255821222248701,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1274,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1392
|
||||
},
|
||||
"BodyFeetPants": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 0.7839642922675355,
|
||||
"edgeRatioMin": 0.513176300659891,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1466,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1602
|
||||
},
|
||||
"BodyFeetPantsShoes": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 0.7799605112083939,
|
||||
"edgeRatioMin": 0.513176300659891,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 830,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1048
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.9536679047270835,
|
||||
"edgeRatioMin": 0.33440855736119546,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3726,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 3373
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.8102305567029533,
|
||||
"edgeRatioMin": 0.26171116386651677,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 5384,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 5046
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.9536679047270835,
|
||||
"edgeRatioMin": 0.33440855736119546,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6228,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5556
|
||||
},
|
||||
"Bottoms": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.86670117423969,
|
||||
"edgeRatioMin": 0.513176300659891,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 826,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 942
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9201631088272872,
|
||||
"edgeRatioMin": 0.9197675922571078,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3839,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4045
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9200000519776095,
|
||||
"edgeRatioMin": 0.9199996919846277,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 4,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9421731351522475,
|
||||
"edgeRatioMin": 0.8873110924578558,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.2562402728849387,
|
||||
"edgeRatioMin": 0.7903516292481316,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 724,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"Panties001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.8213261947723751,
|
||||
"edgeRatioMin": 0.5459879769698383,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 604,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 582
|
||||
},
|
||||
"Pants0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0359715012588415,
|
||||
"edgeRatioMin": 0.5209577970985301,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 898,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 915
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.7799605112083939,
|
||||
"edgeRatioMin": 0.6458808184096261,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.3836635660461891,
|
||||
"edgeRatioMin": 0.26171116386651677,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"TopsEdited": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.3836635660461891,
|
||||
"edgeRatioMin": 0.26171116386651677,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"coat": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 5.7431588938626055,
|
||||
"edgeRatioMin": 0.06297046287549611,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"hair0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.6992910918861586,
|
||||
"edgeRatioMin": 0.8209691490768963,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0002": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.6992910918861586,
|
||||
"edgeRatioMin": 0.8209691490768963,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0003": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"edgeRatioMin": 1.0,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.76872570692281,
|
||||
"edgeRatioMin": 0.6246053643865501,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 242,
|
||||
"ageBones": 242,
|
||||
"profile": {
|
||||
"arm": 0.6,
|
||||
"bodyRadial": 0.78,
|
||||
"foot": 0.75,
|
||||
"head": 0.92,
|
||||
"headBulge": 1.03,
|
||||
"hipHeight": 0.55,
|
||||
"hipWidth": 0.72,
|
||||
"leg": 0.55,
|
||||
"limbRadial": 0.72,
|
||||
"shoulderWidth": 0.7,
|
||||
"torso": 0.62,
|
||||
"walkSpeedScale": 0.75
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 5119,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
0.09,
|
||||
1.4972
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
0.0858,
|
||||
1.6039
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
-0.0184,
|
||||
1.5008
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0344,
|
||||
1.6475
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0636,
|
||||
0.0319,
|
||||
1.6457
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0645,
|
||||
0.0294,
|
||||
1.6445
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0698,
|
||||
0.021,
|
||||
1.649
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0716,
|
||||
0.0206,
|
||||
1.6529
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 7.33739356344359e-06,
|
||||
"snappedVertices": 216
|
||||
},
|
||||
"sex": "male",
|
||||
"status": "warn",
|
||||
"stem": "child-male",
|
||||
"timestamp": 1793419291,
|
||||
"warnings": [
|
||||
"Body",
|
||||
"Body_shapes",
|
||||
"BodyFeetPants",
|
||||
"BodyFeetPantsShoes",
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Tops",
|
||||
"TopsEdited",
|
||||
"coat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Conversion Report — child / male
|
||||
|
||||
- Stem: `child-male`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 25
|
||||
- Warnings: 9
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `male`
|
||||
- ageArmature: `child-male`
|
||||
- animations: `{'scaledLocationCurves': 7601, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014', 'Armature|mixamo.com|Layer0.012'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'child-male'}`
|
||||
- outputBase: `edited-normal-child-male.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-male.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `5119`
|
||||
- snappedVertices: `216`
|
||||
- maxGap: `7.33739356344359e-06`
|
||||
- examples: `[{'position': [0.0, 0.09, 1.4972], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0, 0.0858, 1.6039], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0, -0.0184, 1.5008], 'gap': 2e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0633, 0.0369, 1.6496], 'gap': 2e-06, 'objects': ['BodyTop', 'Face']}, {'position': [-0.0633, 0.0369, 1.6496], 'gap': 2e-06, 'objects': ['BodyTop', 'Face']}, {'position': [0.0633, 0.0344, 1.6475], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0636, 0.0319, 1.6457], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0645, 0.0294, 1.6445], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0698, 0.021, 1.649], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0716, 0.0206, 1.6529], 'gap': 1e-06, 'objects': ['Face']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `Body`
|
||||
- `Body_shapes`
|
||||
- `BodyFeetPants`
|
||||
- `BodyFeetPantsShoes`
|
||||
- `BodyTop`
|
||||
- `BodyTopRobe`
|
||||
- `Tops`
|
||||
- `TopsEdited`
|
||||
- `coat`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — ok; verts=145, tris=0 degenerate, influences=4, unweighted=0, edge=0.48..0.79
|
||||
- `Body` — warn; verts=6584, tris=0 degenerate, influences=4, unweighted=0, edge=0.33..1.95
|
||||
- `BodyBottom` — ok; verts=1181, tris=0 degenerate, influences=4, unweighted=0, edge=0.54..0.82
|
||||
- `BodyBottomPants` — ok; verts=1133, tris=0 degenerate, influences=4, unweighted=0, edge=0.54..0.87
|
||||
- `BodyFeet` — ok; verts=1392, tris=0 degenerate, influences=3, unweighted=0, edge=0.53..0.78
|
||||
- `BodyFeetPants` — warn; verts=1602, tris=48 degenerate, influences=3, unweighted=0, edge=0.51..0.78
|
||||
- `BodyFeetPantsShoes` — warn; verts=1048, tris=48 degenerate, influences=3, unweighted=0, edge=0.51..0.78
|
||||
- `BodyTop` — warn; verts=3373, tris=0 degenerate, influences=4, unweighted=0, edge=0.33..1.95
|
||||
- `BodyTopRobe` — warn; verts=5046, tris=0 degenerate, influences=4, unweighted=0, edge=0.26..1.81
|
||||
- `Body_shapes` — warn; verts=5556, tris=0 degenerate, influences=4, unweighted=0, edge=0.33..1.95
|
||||
- `Bottoms` — ok; verts=942, tris=0 degenerate, influences=4, unweighted=0, edge=0.51..0.87
|
||||
- `Face` — ok; verts=4045, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `Hair` — ok; verts=9, tris=0 degenerate, influences=1, unweighted=0, edge=0.92..0.92
|
||||
- `Hair2` — ok; verts=7022, tris=0 degenerate, influences=2, unweighted=0, edge=0.89..0.94
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=0.79..1.26
|
||||
- `Panties001` — ok; verts=582, tris=0 degenerate, influences=4, unweighted=0, edge=0.55..0.82
|
||||
- `Pants0001` — ok; verts=915, tris=0 degenerate, influences=4, unweighted=0, edge=0.52..1.04
|
||||
- `Shoes` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=0.65..0.78
|
||||
- `Tops` — warn; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.26..1.38
|
||||
- `TopsEdited` — warn; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.26..1.38
|
||||
- `coat` — warn; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.06..5.74
|
||||
- `hair0001` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=0.82..1.70
|
||||
- `hair0002` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=0.82..1.70
|
||||
- `hair0003` — ok; verts=7022, tris=0 degenerate, influences=0, unweighted=0, edge=1.00..1.00
|
||||
- `shoes1` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=0.62..0.77
|
||||
@@ -0,0 +1,621 @@
|
||||
{
|
||||
"age": "elder",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "female",
|
||||
"ageArmature": "elder-female",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7028,
|
||||
"stem": "elder-female"
|
||||
},
|
||||
"outputBase": "edited-normal-elder-female.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-female.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000008309035069,
|
||||
"edgeRatioMin": 0.9999861617983601,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 40,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 69
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000602153963685,
|
||||
"edgeRatioMin": 0.9999297885300813,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6227,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6578
|
||||
},
|
||||
"Body-merged": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000602153963685,
|
||||
"edgeRatioMin": 0.9999297885300813,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 8423,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9178
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000091614229163,
|
||||
"edgeRatioMin": 0.9999833439579573,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1242,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1194
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000004257443259,
|
||||
"edgeRatioMin": 0.9999898988835334,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1280,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1050
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000602153963685,
|
||||
"edgeRatioMin": 0.9999297885300813,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3701,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 3354
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000602153963685,
|
||||
"edgeRatioMin": 0.9999297885300813,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4727,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 4383
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000602153963685,
|
||||
"edgeRatioMin": 0.9999297885300813,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6223,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5550
|
||||
},
|
||||
"Cylinder": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000211541526272,
|
||||
"edgeRatioMin": 0.9999790604099301,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 595,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 840,
|
||||
"vertices": 840
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000190299901157,
|
||||
"edgeRatioMin": 0.9999818716880085,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3887,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4109
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"edgeRatioMin": 1.0,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8
|
||||
},
|
||||
"Hair001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000108986292174,
|
||||
"edgeRatioMin": 0.999757672900018,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000886276531582,
|
||||
"edgeRatioMin": 0.9982467837381087,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000033289511109,
|
||||
"edgeRatioMin": 0.9999827306906097,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 726,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"OnepieceCloth": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.00000327729584,
|
||||
"edgeRatioMin": 0.9999867384343708,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000000954873743,
|
||||
"edgeRatioMin": 0.9999976654518632,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 402,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 608
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000009505554167,
|
||||
"edgeRatioMin": 0.9999842864583346,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1026,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1029
|
||||
},
|
||||
"bra1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000653723155617,
|
||||
"edgeRatioMin": 0.9999349455725334,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 430,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 419
|
||||
},
|
||||
"bra2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000515467022928,
|
||||
"edgeRatioMin": 0.9999485610532655,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 506,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 524
|
||||
},
|
||||
"bra3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000045760529744,
|
||||
"edgeRatioMin": 0.9999485610532655,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"bra4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000045760529744,
|
||||
"edgeRatioMin": 0.9999485610532655,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"panties": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000105452390367,
|
||||
"edgeRatioMin": 0.9999622354677439,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 674,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 797,
|
||||
"vertices": 797
|
||||
},
|
||||
"panties-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000048680725775,
|
||||
"edgeRatioMin": 0.9999761306308599,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 337,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 425,
|
||||
"vertices": 425
|
||||
},
|
||||
"panties2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000202864294472,
|
||||
"edgeRatioMin": 0.9999826178387231,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 1266,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 1328,
|
||||
"vertices": 1328
|
||||
},
|
||||
"panties2-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000121089841207,
|
||||
"edgeRatioMin": 0.9999863856203022,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 507,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 558,
|
||||
"vertices": 558
|
||||
},
|
||||
"panties3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000139098043757,
|
||||
"edgeRatioMin": 0.9999866822085812,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 758,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 820,
|
||||
"vertices": 820
|
||||
},
|
||||
"panties4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000139098043757,
|
||||
"edgeRatioMin": 0.9999866822085812,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 690,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 752,
|
||||
"vertices": 752
|
||||
},
|
||||
"panties5": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000461926176676,
|
||||
"edgeRatioMin": 0.9999441034118597,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties6": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000277280519727,
|
||||
"edgeRatioMin": 0.9999767160991722,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties7": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000385836476469,
|
||||
"edgeRatioMin": 0.9999691785976204,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000001217005389,
|
||||
"edgeRatioMin": 0.9999980533858904,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 414,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 414
|
||||
},
|
||||
"skirt1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000003778236853,
|
||||
"edgeRatioMin": 0.9999955122967341,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"skirt2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000005985850434,
|
||||
"edgeRatioMin": 0.999995191044834,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 154,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 176
|
||||
},
|
||||
"skirt3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000032530300955,
|
||||
"edgeRatioMin": 0.9999941815040422,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"top1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000349169435994,
|
||||
"edgeRatioMin": 0.9999356995377789,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 860,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 852
|
||||
},
|
||||
"top2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000115228837376,
|
||||
"edgeRatioMin": 0.9999867173552025,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
},
|
||||
"top3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000115228837376,
|
||||
"edgeRatioMin": 0.9999867173552025,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 252,
|
||||
"ageBones": 252,
|
||||
"profile": {
|
||||
"arm": 1.0,
|
||||
"bodyRadial": 1.0,
|
||||
"foot": 1.0,
|
||||
"head": 1.0,
|
||||
"headBulge": 1.0,
|
||||
"hipHeight": 1.0,
|
||||
"hipWidth": 1.0,
|
||||
"leg": 1.0,
|
||||
"limbRadial": 1.0,
|
||||
"shoulderWidth": 1.0,
|
||||
"torso": 1.0,
|
||||
"walkSpeedScale": 1.0
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 3697,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0012,
|
||||
-0.0342,
|
||||
1.5629
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.001,
|
||||
-0.0343,
|
||||
1.5628
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 7.0412131889888606e-06,
|
||||
"snappedVertices": 95
|
||||
},
|
||||
"sex": "female",
|
||||
"status": "warn",
|
||||
"stem": "elder-female",
|
||||
"timestamp": 1793419268,
|
||||
"warnings": [
|
||||
"Cylinder",
|
||||
"panties",
|
||||
"panties-full",
|
||||
"panties2",
|
||||
"panties2-full",
|
||||
"panties3",
|
||||
"panties4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# Conversion Report — elder / female
|
||||
|
||||
- Stem: `elder-female`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 37
|
||||
- Warnings: 7
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `female`
|
||||
- ageArmature: `elder-female`
|
||||
- animations: `{'scaledLocationCurves': 7028, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'elder-female'}`
|
||||
- outputBase: `edited-normal-elder-female.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-female.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `3697`
|
||||
- snappedVertices: `95`
|
||||
- maxGap: `7.0412131889888606e-06`
|
||||
- examples: `[{'position': [0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0012, -0.0342, 1.5629], 'gap': 3e-06, 'objects': ['Hair2']}, {'position': [-0.001, -0.0343, 1.5628], 'gap': 3e-06, 'objects': ['Hair2']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `Cylinder`
|
||||
- `panties`
|
||||
- `panties-full`
|
||||
- `panties2`
|
||||
- `panties2-full`
|
||||
- `panties3`
|
||||
- `panties4`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — ok; verts=69, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Body` — ok; verts=6578, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Body-merged` — ok; verts=9178, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyBottom` — ok; verts=1194, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyFeet` — ok; verts=1050, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `BodyTop` — ok; verts=3354, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyTopRobe` — ok; verts=4383, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Body_shapes` — ok; verts=5550, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Cylinder` — warn; verts=840, tris=0 degenerate, influences=0, unweighted=840, edge=1.00..1.00
|
||||
- `Face` — ok; verts=4109, tris=0 degenerate, influences=1, unweighted=0, edge=1.00..1.00
|
||||
- `Hair` — ok; verts=8, tris=0 degenerate, influences=1, unweighted=0, edge=1.00..1.00
|
||||
- `Hair001` — ok; verts=8673, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Hair2` — ok; verts=8673, tris=0 degenerate, influences=2, unweighted=0, edge=1.00..1.00
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `OnepieceCloth` — ok; verts=6, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `Shoes` — ok; verts=608, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `Tops` — ok; verts=1029, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `bra1` — ok; verts=419, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `bra2` — ok; verts=524, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `bra3` — ok; verts=450, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `bra4` — ok; verts=450, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `panties` — warn; verts=797, tris=0 degenerate, influences=0, unweighted=797, edge=1.00..1.00
|
||||
- `panties-full` — warn; verts=425, tris=0 degenerate, influences=0, unweighted=425, edge=1.00..1.00
|
||||
- `panties2` — warn; verts=1328, tris=0 degenerate, influences=0, unweighted=1328, edge=1.00..1.00
|
||||
- `panties2-full` — warn; verts=558, tris=0 degenerate, influences=0, unweighted=558, edge=1.00..1.00
|
||||
- `panties3` — warn; verts=820, tris=0 degenerate, influences=0, unweighted=820, edge=1.00..1.00
|
||||
- `panties4` — warn; verts=752, tris=0 degenerate, influences=0, unweighted=752, edge=1.00..1.00
|
||||
- `panties5` — ok; verts=758, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `panties6` — ok; verts=758, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `panties7` — ok; verts=758, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `shoes1` — ok; verts=414, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `skirt1` — ok; verts=154, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `skirt2` — ok; verts=176, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `skirt3` — ok; verts=154, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `top1` — ok; verts=852, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `top2` — ok; verts=854, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `top3` — ok; verts=854, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
@@ -0,0 +1,482 @@
|
||||
{
|
||||
"age": "elder",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "male",
|
||||
"ageArmature": "elder-male",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014",
|
||||
"Armature|mixamo.com|Layer0.012"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7601,
|
||||
"stem": "elder-male"
|
||||
},
|
||||
"outputBase": "edited-normal-elder-male.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-male.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000019766352531,
|
||||
"edgeRatioMin": 0.9999805255229,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 111,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 145
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000897554304082,
|
||||
"edgeRatioMin": 0.9999611641687194,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6232,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 6584
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000079995934277,
|
||||
"edgeRatioMin": 0.999989253410586,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1228,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1181
|
||||
},
|
||||
"BodyBottomPants": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000258231043282,
|
||||
"edgeRatioMin": 0.9999859573234091,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 972,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1133
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000046342516928,
|
||||
"edgeRatioMin": 0.9999981174339364,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1274,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1392
|
||||
},
|
||||
"BodyFeetPants": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 1.0000046342516928,
|
||||
"edgeRatioMin": 0.9999981174339364,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1466,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1602
|
||||
},
|
||||
"BodyFeetPantsShoes": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 1.0000013400794516,
|
||||
"edgeRatioMin": 0.9999981174339364,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 830,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1048
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000897554304082,
|
||||
"edgeRatioMin": 0.9999611641687194,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3726,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 3373
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000897554304082,
|
||||
"edgeRatioMin": 0.9999611641687194,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 5384,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 5046
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000897554304082,
|
||||
"edgeRatioMin": 0.9999611641687194,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6228,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5556
|
||||
},
|
||||
"Bottoms": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000258231043282,
|
||||
"edgeRatioMin": 0.9999859573234091,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 826,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 942
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000018813696014,
|
||||
"edgeRatioMin": 0.9999982328372087,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3839,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4045
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"edgeRatioMin": 0.9999997540155123,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 4,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000542889561435,
|
||||
"edgeRatioMin": 0.999194851963821,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000295096813758,
|
||||
"edgeRatioMin": 0.9999786138678867,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 724,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"Panties001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0010504381569287,
|
||||
"edgeRatioMin": 0.9991726553447599,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 604,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 582
|
||||
},
|
||||
"Pants0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000229696457892,
|
||||
"edgeRatioMin": 0.9999874704829604,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 898,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 915
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000013174267397,
|
||||
"edgeRatioMin": 0.9999985866462575,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.00000962524978,
|
||||
"edgeRatioMin": 0.9999900806166673,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"TopsEdited": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.00000962524978,
|
||||
"edgeRatioMin": 0.9999900806166673,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"coat": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.000022626000937,
|
||||
"edgeRatioMin": 0.9999831391448877,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"hair0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000358941381666,
|
||||
"edgeRatioMin": 0.99983153357466,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0002": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000358941381666,
|
||||
"edgeRatioMin": 0.99983153357466,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0003": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"edgeRatioMin": 1.0,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0000013174267397,
|
||||
"edgeRatioMin": 0.9999985251538689,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 242,
|
||||
"ageBones": 242,
|
||||
"profile": {
|
||||
"arm": 1.0,
|
||||
"bodyRadial": 1.0,
|
||||
"foot": 1.0,
|
||||
"head": 1.0,
|
||||
"headBulge": 1.0,
|
||||
"hipHeight": 1.0,
|
||||
"hipWidth": 1.0,
|
||||
"leg": 1.0,
|
||||
"limbRadial": 1.0,
|
||||
"shoulderWidth": 1.0,
|
||||
"torso": 1.0,
|
||||
"walkSpeedScale": 1.0
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 5119,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
0.09,
|
||||
1.4972
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
0.0858,
|
||||
1.6039
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
-0.0184,
|
||||
1.5008
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0842,
|
||||
0.0277,
|
||||
1.6955
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0842,
|
||||
0.0277,
|
||||
1.6955
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0344,
|
||||
1.6475
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0636,
|
||||
0.0319,
|
||||
1.6457
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0645,
|
||||
0.0294,
|
||||
1.6445
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 8.047489926985063e-06,
|
||||
"snappedVertices": 226
|
||||
},
|
||||
"sex": "male",
|
||||
"status": "warn",
|
||||
"stem": "elder-male",
|
||||
"timestamp": 1793419262,
|
||||
"warnings": [
|
||||
"BodyFeetPants",
|
||||
"BodyFeetPantsShoes"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# Conversion Report — elder / male
|
||||
|
||||
- Stem: `elder-male`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 25
|
||||
- Warnings: 2
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `male`
|
||||
- ageArmature: `elder-male`
|
||||
- animations: `{'scaledLocationCurves': 7601, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014', 'Armature|mixamo.com|Layer0.012'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'elder-male'}`
|
||||
- outputBase: `edited-normal-elder-male.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-male.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `5119`
|
||||
- snappedVertices: `226`
|
||||
- maxGap: `8.047489926985063e-06`
|
||||
- examples: `[{'position': [0.0, 0.09, 1.4972], 'gap': 2e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0, 0.0858, 1.6039], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0, -0.0184, 1.5008], 'gap': 2e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0633, 0.0369, 1.6496], 'gap': 2e-06, 'objects': ['BodyTop', 'Face']}, {'position': [-0.0633, 0.0369, 1.6496], 'gap': 2e-06, 'objects': ['BodyTop', 'Face']}, {'position': [0.0842, 0.0277, 1.6955], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe', 'Face']}, {'position': [-0.0842, 0.0277, 1.6955], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe', 'Face']}, {'position': [0.0633, 0.0344, 1.6475], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0636, 0.0319, 1.6457], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0645, 0.0294, 1.6445], 'gap': 1e-06, 'objects': ['Face']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `BodyFeetPants`
|
||||
- `BodyFeetPantsShoes`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — ok; verts=145, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Body` — ok; verts=6584, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyBottom` — ok; verts=1181, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyBottomPants` — ok; verts=1133, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyFeet` — ok; verts=1392, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `BodyFeetPants` — warn; verts=1602, tris=48 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `BodyFeetPantsShoes` — warn; verts=1048, tris=48 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `BodyTop` — ok; verts=3373, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `BodyTopRobe` — ok; verts=5046, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Body_shapes` — ok; verts=5556, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Bottoms` — ok; verts=942, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Face` — ok; verts=4045, tris=0 degenerate, influences=1, unweighted=0, edge=1.00..1.00
|
||||
- `Hair` — ok; verts=9, tris=0 degenerate, influences=1, unweighted=0, edge=1.00..1.00
|
||||
- `Hair2` — ok; verts=7022, tris=0 degenerate, influences=2, unweighted=0, edge=1.00..1.00
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Panties001` — ok; verts=582, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Pants0001` — ok; verts=915, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `Shoes` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
- `Tops` — ok; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `TopsEdited` — ok; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `coat` — ok; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `hair0001` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `hair0002` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=1.00..1.00
|
||||
- `hair0003` — ok; verts=7022, tris=0 degenerate, influences=0, unweighted=0, edge=1.00..1.00
|
||||
- `shoes1` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=1.00..1.00
|
||||
@@ -0,0 +1,622 @@
|
||||
{
|
||||
"age": "teenager",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "female",
|
||||
"ageArmature": "teenager-female",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7028,
|
||||
"stem": "teenager-female"
|
||||
},
|
||||
"outputBase": "edited-normal-teenager-female.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-female.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9428835642440181,
|
||||
"edgeRatioMin": 0.8937664900167279,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 40,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 69
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.153864673534735,
|
||||
"edgeRatioMin": 0.8788689898808826,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6227,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6578
|
||||
},
|
||||
"Body-merged": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.153864673534735,
|
||||
"edgeRatioMin": 0.875585733388662,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 8423,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9178
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9413767566501527,
|
||||
"edgeRatioMin": 0.894824475491785,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1242,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1194
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.01533051589042,
|
||||
"edgeRatioMin": 0.8905319834891143,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1280,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1050
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.153864673534735,
|
||||
"edgeRatioMin": 0.8788684699247973,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3701,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 3354
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.153864673534735,
|
||||
"edgeRatioMin": 0.875585733388662,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4727,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 4383
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.153864673534735,
|
||||
"edgeRatioMin": 0.8788689898808826,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6223,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5550
|
||||
},
|
||||
"Cylinder": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 13.149501164154342,
|
||||
"edgeRatioMin": 0.2067502446063823,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 595,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 840,
|
||||
"vertices": 840
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9900810993796347,
|
||||
"edgeRatioMin": 0.9896786488081534,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3887,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4109
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9900007559563859,
|
||||
"edgeRatioMin": 0.9899997124609613,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8
|
||||
},
|
||||
"Hair001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.3437205849900313,
|
||||
"edgeRatioMin": 0.39954414397197524,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9961437951038233,
|
||||
"edgeRatioMin": 0.9739232308631328,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 5658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 8673
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0706959837595011,
|
||||
"edgeRatioMin": 0.9495467764182186,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 726,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"OnepieceCloth": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9399357283296659,
|
||||
"edgeRatioMin": 0.9381366728208823,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 2,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 6
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9981897700090412,
|
||||
"edgeRatioMin": 0.9254076761648661,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 402,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 608
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9511826460744913,
|
||||
"edgeRatioMin": 0.875585733388662,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1026,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1029
|
||||
},
|
||||
"bra1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9900000946583577,
|
||||
"edgeRatioMin": 0.2887434501951809,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 430,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 419
|
||||
},
|
||||
"bra2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9984894682233404,
|
||||
"edgeRatioMin": 0.4006806605346238,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 506,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 524
|
||||
},
|
||||
"bra3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9984894682233404,
|
||||
"edgeRatioMin": 0.4889485839438301,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"bra4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9984894682233404,
|
||||
"edgeRatioMin": 0.4889485839438301,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 410,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 450
|
||||
},
|
||||
"panties": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 50.21989080149669,
|
||||
"edgeRatioMin": 0.19000501701728584,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 674,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 797,
|
||||
"vertices": 797
|
||||
},
|
||||
"panties-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.3300942912650158,
|
||||
"edgeRatioMin": 0.12847562281583486,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 337,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 425,
|
||||
"vertices": 425
|
||||
},
|
||||
"panties2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 10.548253137802863,
|
||||
"edgeRatioMin": 0.3920225273569097,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 1266,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 1328,
|
||||
"vertices": 1328
|
||||
},
|
||||
"panties2-full": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 9.15854640358302,
|
||||
"edgeRatioMin": 0.6364910348952761,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 507,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 558,
|
||||
"vertices": 558
|
||||
},
|
||||
"panties3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.299860127337894,
|
||||
"edgeRatioMin": 0.13403860939637366,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 758,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 820,
|
||||
"vertices": 820
|
||||
},
|
||||
"panties4": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 7.740842499445837,
|
||||
"edgeRatioMin": 0.1158857342919668,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 690,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 752,
|
||||
"vertices": 752
|
||||
},
|
||||
"panties5": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9443143846802673,
|
||||
"edgeRatioMin": 0.8965532928515044,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties6": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.95104115229466,
|
||||
"edgeRatioMin": 0.8819173360530367,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"panties7": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9455920513906987,
|
||||
"edgeRatioMin": 0.8823391488934409,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 698,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 758
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9807207937208285,
|
||||
"edgeRatioMin": 0.9278301779320385,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 414,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 414
|
||||
},
|
||||
"skirt1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9422412937744205,
|
||||
"edgeRatioMin": 0.8944357351478923,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"skirt2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9422412937744205,
|
||||
"edgeRatioMin": 0.8944357351478923,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 154,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 176
|
||||
},
|
||||
"skirt3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9444972809207006,
|
||||
"edgeRatioMin": 0.9023806027952785,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 132,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 154
|
||||
},
|
||||
"top1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0484285122626043,
|
||||
"edgeRatioMin": 0.5756344323442318,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 860,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 852
|
||||
},
|
||||
"top2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0484285122626043,
|
||||
"edgeRatioMin": 0.5756344323442318,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
},
|
||||
"top3": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0484285122626043,
|
||||
"edgeRatioMin": 0.5756344323442318,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 876,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 854
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 252,
|
||||
"ageBones": 252,
|
||||
"profile": {
|
||||
"arm": 0.92,
|
||||
"bodyRadial": 0.94,
|
||||
"foot": 0.97,
|
||||
"head": 0.99,
|
||||
"headBulge": 1.0,
|
||||
"hipHeight": 0.94,
|
||||
"hipWidth": 0.92,
|
||||
"leg": 0.94,
|
||||
"limbRadial": 0.93,
|
||||
"shoulderWidth": 0.92,
|
||||
"torso": 0.92,
|
||||
"walkSpeedScale": 0.95
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 3697,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0649,
|
||||
0.0281,
|
||||
1.3797
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.066,
|
||||
0.0231,
|
||||
1.3767
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0694,
|
||||
0.0172,
|
||||
1.3783
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0001,
|
||||
-0.0385,
|
||||
1.5529
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.0012,
|
||||
-0.0342,
|
||||
1.5629
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 3e-06,
|
||||
"objects": [
|
||||
"Hair2"
|
||||
],
|
||||
"position": [
|
||||
-0.001,
|
||||
-0.0343,
|
||||
1.5628
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 6.944388526471647e-06,
|
||||
"snappedVertices": 95
|
||||
},
|
||||
"sex": "female",
|
||||
"status": "warn",
|
||||
"stem": "teenager-female",
|
||||
"timestamp": 1793419282,
|
||||
"warnings": [
|
||||
"bra1",
|
||||
"Cylinder",
|
||||
"panties",
|
||||
"panties-full",
|
||||
"panties2",
|
||||
"panties2-full",
|
||||
"panties3",
|
||||
"panties4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Conversion Report — teenager / female
|
||||
|
||||
- Stem: `teenager-female`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 37
|
||||
- Warnings: 8
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `female`
|
||||
- ageArmature: `teenager-female`
|
||||
- animations: `{'scaledLocationCurves': 7028, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'teenager-female'}`
|
||||
- outputBase: `edited-normal-teenager-female.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-female.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `3697`
|
||||
- snappedVertices: `95`
|
||||
- maxGap: `6.944388526471647e-06`
|
||||
- examples: `[{'position': [0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0649, 0.0281, 1.3797], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.066, 0.0231, 1.3767], 'gap': 1e-06, 'objects': ['Face']}, {'position': [-0.0694, 0.0172, 1.3783], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0001, -0.0385, 1.5529], 'gap': 2e-06, 'objects': ['Hair2']}, {'position': [-0.0012, -0.0342, 1.5629], 'gap': 3e-06, 'objects': ['Hair2']}, {'position': [-0.001, -0.0343, 1.5628], 'gap': 3e-06, 'objects': ['Hair2']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `bra1`
|
||||
- `Cylinder`
|
||||
- `panties`
|
||||
- `panties-full`
|
||||
- `panties2`
|
||||
- `panties2-full`
|
||||
- `panties3`
|
||||
- `panties4`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — ok; verts=69, tris=0 degenerate, influences=4, unweighted=0, edge=0.89..0.94
|
||||
- `Body` — ok; verts=6578, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..1.15
|
||||
- `Body-merged` — ok; verts=9178, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..1.15
|
||||
- `BodyBottom` — ok; verts=1194, tris=0 degenerate, influences=4, unweighted=0, edge=0.89..0.94
|
||||
- `BodyFeet` — ok; verts=1050, tris=0 degenerate, influences=3, unweighted=0, edge=0.89..1.02
|
||||
- `BodyTop` — ok; verts=3354, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..1.15
|
||||
- `BodyTopRobe` — ok; verts=4383, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..1.15
|
||||
- `Body_shapes` — ok; verts=5550, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..1.15
|
||||
- `Cylinder` — warn; verts=840, tris=0 degenerate, influences=0, unweighted=840, edge=0.21..13.15
|
||||
- `Face` — ok; verts=4109, tris=0 degenerate, influences=1, unweighted=0, edge=0.99..0.99
|
||||
- `Hair` — ok; verts=8, tris=0 degenerate, influences=1, unweighted=0, edge=0.99..0.99
|
||||
- `Hair001` — ok; verts=8673, tris=0 degenerate, influences=4, unweighted=0, edge=0.40..1.34
|
||||
- `Hair2` — ok; verts=8673, tris=0 degenerate, influences=2, unweighted=0, edge=0.97..1.00
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=0.95..1.07
|
||||
- `OnepieceCloth` — ok; verts=6, tris=0 degenerate, influences=3, unweighted=0, edge=0.94..0.94
|
||||
- `Shoes` — ok; verts=608, tris=0 degenerate, influences=3, unweighted=0, edge=0.93..1.00
|
||||
- `Tops` — ok; verts=1029, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..0.95
|
||||
- `bra1` — warn; verts=419, tris=0 degenerate, influences=4, unweighted=0, edge=0.29..0.99
|
||||
- `bra2` — ok; verts=524, tris=0 degenerate, influences=4, unweighted=0, edge=0.40..1.00
|
||||
- `bra3` — ok; verts=450, tris=0 degenerate, influences=4, unweighted=0, edge=0.49..1.00
|
||||
- `bra4` — ok; verts=450, tris=0 degenerate, influences=4, unweighted=0, edge=0.49..1.00
|
||||
- `panties` — warn; verts=797, tris=0 degenerate, influences=0, unweighted=797, edge=0.19..50.22
|
||||
- `panties-full` — warn; verts=425, tris=0 degenerate, influences=0, unweighted=425, edge=0.13..1.33
|
||||
- `panties2` — warn; verts=1328, tris=0 degenerate, influences=0, unweighted=1328, edge=0.39..10.55
|
||||
- `panties2-full` — warn; verts=558, tris=0 degenerate, influences=0, unweighted=558, edge=0.64..9.16
|
||||
- `panties3` — warn; verts=820, tris=0 degenerate, influences=0, unweighted=820, edge=0.13..7.30
|
||||
- `panties4` — warn; verts=752, tris=0 degenerate, influences=0, unweighted=752, edge=0.12..7.74
|
||||
- `panties5` — ok; verts=758, tris=0 degenerate, influences=4, unweighted=0, edge=0.90..0.94
|
||||
- `panties6` — ok; verts=758, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..0.95
|
||||
- `panties7` — ok; verts=758, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..0.95
|
||||
- `shoes1` — ok; verts=414, tris=0 degenerate, influences=3, unweighted=0, edge=0.93..0.98
|
||||
- `skirt1` — ok; verts=154, tris=0 degenerate, influences=4, unweighted=0, edge=0.89..0.94
|
||||
- `skirt2` — ok; verts=176, tris=0 degenerate, influences=4, unweighted=0, edge=0.89..0.94
|
||||
- `skirt3` — ok; verts=154, tris=0 degenerate, influences=4, unweighted=0, edge=0.90..0.94
|
||||
- `top1` — ok; verts=852, tris=0 degenerate, influences=4, unweighted=0, edge=0.58..1.05
|
||||
- `top2` — ok; verts=854, tris=0 degenerate, influences=4, unweighted=0, edge=0.58..1.05
|
||||
- `top3` — ok; verts=854, tris=0 degenerate, influences=4, unweighted=0, edge=0.58..1.05
|
||||
@@ -0,0 +1,482 @@
|
||||
{
|
||||
"age": "teenager",
|
||||
"fails": [],
|
||||
"info": {
|
||||
"adultArmature": "male",
|
||||
"ageArmature": "teenager-male",
|
||||
"animations": {
|
||||
"aliased": {
|
||||
"swimming-fast": "swimming"
|
||||
},
|
||||
"appended": [
|
||||
"Armature|mixamo.com|Layer0.020",
|
||||
"Armature|mixamo.com|Layer0.018",
|
||||
"Armature|mixamo.com|Layer0.016",
|
||||
"Armature|mixamo.com|Layer0.014",
|
||||
"Armature|mixamo.com|Layer0.012"
|
||||
],
|
||||
"missing": [],
|
||||
"scaledLocationCurves": 7601,
|
||||
"stem": "teenager-male"
|
||||
},
|
||||
"outputBase": "edited-normal-teenager-male.blend",
|
||||
"slots": "bottom,face,feet,hair,top",
|
||||
"sourceBase": "edited-normal-male.blend"
|
||||
},
|
||||
"objects": {
|
||||
"Accessory": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9408520391417841,
|
||||
"edgeRatioMin": 0.9129875147754315,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 111,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 145
|
||||
},
|
||||
"Body": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.218153566216659,
|
||||
"edgeRatioMin": 0.7349989979664033,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6232,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 6584
|
||||
},
|
||||
"BodyBottom": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9415051179583105,
|
||||
"edgeRatioMin": 0.8926428739692449,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1228,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1181
|
||||
},
|
||||
"BodyBottomPants": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9409140829633826,
|
||||
"edgeRatioMin": 0.8827862876547917,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 972,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1133
|
||||
},
|
||||
"BodyFeet": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0153534321863444,
|
||||
"edgeRatioMin": 0.8887109284611726,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1274,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 1392
|
||||
},
|
||||
"BodyFeetPants": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 1.0153534321863444,
|
||||
"edgeRatioMin": 0.8887109284611726,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 1466,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1602
|
||||
},
|
||||
"BodyFeetPantsShoes": {
|
||||
"degenerateTris": 48,
|
||||
"edgeRatioMax": 1.0099758747299858,
|
||||
"edgeRatioMin": 0.9253361353429929,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 830,
|
||||
"quality": "warn",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1048
|
||||
},
|
||||
"BodyTop": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.218153566216659,
|
||||
"edgeRatioMin": 0.7349989979664033,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 3726,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 3373
|
||||
},
|
||||
"BodyTopRobe": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.1954519257878777,
|
||||
"edgeRatioMin": 0.7349989979664033,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 5384,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 1,
|
||||
"unweighted": 0,
|
||||
"vertices": 5046
|
||||
},
|
||||
"Body_shapes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.218153566216659,
|
||||
"edgeRatioMin": 0.7349989979664033,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 6228,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 2,
|
||||
"unweighted": 0,
|
||||
"vertices": 5556
|
||||
},
|
||||
"Bottoms": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9635867444531326,
|
||||
"edgeRatioMin": 0.8827862876547917,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 826,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 942
|
||||
},
|
||||
"Face": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9902318510318413,
|
||||
"edgeRatioMin": 0.9899010785060705,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 3839,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 57,
|
||||
"unweighted": 0,
|
||||
"vertices": 4045
|
||||
},
|
||||
"Hair": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9900002385614128,
|
||||
"edgeRatioMin": 0.9899999851477491,
|
||||
"maxInfluences": 1,
|
||||
"note": "",
|
||||
"polygons": 4,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 9
|
||||
},
|
||||
"Hair2": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9990771032056067,
|
||||
"edgeRatioMin": 0.949542700174737,
|
||||
"maxInfluences": 2,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"HairBack": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0723475348632554,
|
||||
"edgeRatioMin": 0.9495563211922972,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 724,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 760
|
||||
},
|
||||
"Panties001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9414287020897463,
|
||||
"edgeRatioMin": 0.8949860611150358,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 604,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 582
|
||||
},
|
||||
"Pants0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9741146771468555,
|
||||
"edgeRatioMin": 0.7167993480571109,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 898,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 915
|
||||
},
|
||||
"Shoes": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0099758747299858,
|
||||
"edgeRatioMin": 0.9253361353429929,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
},
|
||||
"Tops": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.058850186591336,
|
||||
"edgeRatioMin": 0.8334681396211425,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"TopsEdited": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.058850186591336,
|
||||
"edgeRatioMin": 0.8334681396211425,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"coat": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9465083443563386,
|
||||
"edgeRatioMin": 0.8345250576121954,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 1658,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 1896
|
||||
},
|
||||
"hair0001": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.1823451224746337,
|
||||
"edgeRatioMin": 0.949542700174737,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0002": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.1823451224746337,
|
||||
"edgeRatioMin": 0.949542700174737,
|
||||
"maxInfluences": 4,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"hair0003": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"edgeRatioMin": 1.0,
|
||||
"maxInfluences": 0,
|
||||
"note": "",
|
||||
"polygons": 4483,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 7022
|
||||
},
|
||||
"shoes1": {
|
||||
"degenerateTris": 0,
|
||||
"edgeRatioMax": 0.9949808923340873,
|
||||
"edgeRatioMin": 0.9268173377136528,
|
||||
"maxInfluences": 3,
|
||||
"note": "",
|
||||
"polygons": 334,
|
||||
"quality": "ok",
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"vertices": 478
|
||||
}
|
||||
},
|
||||
"profileHash": "dd6901d4220ae704",
|
||||
"rig": {
|
||||
"adultBones": 242,
|
||||
"ageBones": 242,
|
||||
"profile": {
|
||||
"arm": 0.92,
|
||||
"bodyRadial": 0.94,
|
||||
"foot": 0.97,
|
||||
"head": 0.99,
|
||||
"headBulge": 1.0,
|
||||
"hipHeight": 0.94,
|
||||
"hipWidth": 0.92,
|
||||
"leg": 0.94,
|
||||
"limbRadial": 0.93,
|
||||
"shoulderWidth": 0.92,
|
||||
"torso": 0.92,
|
||||
"walkSpeedScale": 0.95
|
||||
},
|
||||
"restOrientationErrors": []
|
||||
},
|
||||
"seams": {
|
||||
"coincidentGroups": 5119,
|
||||
"examples": [
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
0.09,
|
||||
1.4972
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
0.0858,
|
||||
1.6039
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe"
|
||||
],
|
||||
"position": [
|
||||
0.0,
|
||||
-0.0184,
|
||||
1.5008
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 2e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0633,
|
||||
0.0369,
|
||||
1.6496
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0842,
|
||||
0.0277,
|
||||
1.6955
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"BodyTop",
|
||||
"BodyTopRobe",
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
-0.0842,
|
||||
0.0277,
|
||||
1.6955
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0633,
|
||||
0.0344,
|
||||
1.6475
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0636,
|
||||
0.0319,
|
||||
1.6457
|
||||
]
|
||||
},
|
||||
{
|
||||
"gap": 1e-06,
|
||||
"objects": [
|
||||
"Face"
|
||||
],
|
||||
"position": [
|
||||
0.0645,
|
||||
0.0294,
|
||||
1.6445
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxGap": 7.936056721462239e-06,
|
||||
"snappedVertices": 226
|
||||
},
|
||||
"sex": "male",
|
||||
"status": "warn",
|
||||
"stem": "teenager-male",
|
||||
"timestamp": 1793419277,
|
||||
"warnings": [
|
||||
"BodyFeetPants",
|
||||
"BodyFeetPantsShoes"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
# Conversion Report — teenager / male
|
||||
|
||||
- Stem: `teenager-male`
|
||||
- Status: **warn**
|
||||
- Profile hash: `dd6901d4220ae704`
|
||||
- Objects: 25
|
||||
- Warnings: 2
|
||||
- Fails: 0
|
||||
|
||||
## Info
|
||||
- adultArmature: `male`
|
||||
- ageArmature: `teenager-male`
|
||||
- animations: `{'scaledLocationCurves': 7601, 'appended': ['Armature|mixamo.com|Layer0.020', 'Armature|mixamo.com|Layer0.018', 'Armature|mixamo.com|Layer0.016', 'Armature|mixamo.com|Layer0.014', 'Armature|mixamo.com|Layer0.012'], 'aliased': {'swimming-fast': 'swimming'}, 'missing': [], 'stem': 'teenager-male'}`
|
||||
- outputBase: `edited-normal-teenager-male.blend`
|
||||
- slots: `bottom,face,feet,hair,top`
|
||||
- sourceBase: `edited-normal-male.blend`
|
||||
|
||||
## Seams
|
||||
- coincidentGroups: `5119`
|
||||
- snappedVertices: `226`
|
||||
- maxGap: `7.936056721462239e-06`
|
||||
- examples: `[{'position': [0.0, 0.09, 1.4972], 'gap': 2e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0, 0.0858, 1.6039], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0, -0.0184, 1.5008], 'gap': 2e-06, 'objects': ['BodyTop', 'BodyTopRobe']}, {'position': [0.0633, 0.0369, 1.6496], 'gap': 2e-06, 'objects': ['BodyTop', 'Face']}, {'position': [-0.0633, 0.0369, 1.6496], 'gap': 2e-06, 'objects': ['BodyTop', 'Face']}, {'position': [0.0842, 0.0277, 1.6955], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe', 'Face']}, {'position': [-0.0842, 0.0277, 1.6955], 'gap': 1e-06, 'objects': ['BodyTop', 'BodyTopRobe', 'Face']}, {'position': [0.0633, 0.0344, 1.6475], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0636, 0.0319, 1.6457], 'gap': 1e-06, 'objects': ['Face']}, {'position': [0.0645, 0.0294, 1.6445], 'gap': 1e-06, 'objects': ['Face']}]`
|
||||
|
||||
## Warnings (first 20)
|
||||
|
||||
- `BodyFeetPants`
|
||||
- `BodyFeetPantsShoes`
|
||||
|
||||
## Objects
|
||||
|
||||
- `Accessory` — ok; verts=145, tris=0 degenerate, influences=4, unweighted=0, edge=0.91..0.94
|
||||
- `Body` — ok; verts=6584, tris=0 degenerate, influences=4, unweighted=0, edge=0.73..1.22
|
||||
- `BodyBottom` — ok; verts=1181, tris=0 degenerate, influences=4, unweighted=0, edge=0.89..0.94
|
||||
- `BodyBottomPants` — ok; verts=1133, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..0.94
|
||||
- `BodyFeet` — ok; verts=1392, tris=0 degenerate, influences=3, unweighted=0, edge=0.89..1.02
|
||||
- `BodyFeetPants` — warn; verts=1602, tris=48 degenerate, influences=3, unweighted=0, edge=0.89..1.02
|
||||
- `BodyFeetPantsShoes` — warn; verts=1048, tris=48 degenerate, influences=3, unweighted=0, edge=0.93..1.01
|
||||
- `BodyTop` — ok; verts=3373, tris=0 degenerate, influences=4, unweighted=0, edge=0.73..1.22
|
||||
- `BodyTopRobe` — ok; verts=5046, tris=0 degenerate, influences=4, unweighted=0, edge=0.73..1.20
|
||||
- `Body_shapes` — ok; verts=5556, tris=0 degenerate, influences=4, unweighted=0, edge=0.73..1.22
|
||||
- `Bottoms` — ok; verts=942, tris=0 degenerate, influences=4, unweighted=0, edge=0.88..0.96
|
||||
- `Face` — ok; verts=4045, tris=0 degenerate, influences=1, unweighted=0, edge=0.99..0.99
|
||||
- `Hair` — ok; verts=9, tris=0 degenerate, influences=1, unweighted=0, edge=0.99..0.99
|
||||
- `Hair2` — ok; verts=7022, tris=0 degenerate, influences=2, unweighted=0, edge=0.95..1.00
|
||||
- `HairBack` — ok; verts=760, tris=0 degenerate, influences=4, unweighted=0, edge=0.95..1.07
|
||||
- `Panties001` — ok; verts=582, tris=0 degenerate, influences=4, unweighted=0, edge=0.89..0.94
|
||||
- `Pants0001` — ok; verts=915, tris=0 degenerate, influences=4, unweighted=0, edge=0.72..0.97
|
||||
- `Shoes` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=0.93..1.01
|
||||
- `Tops` — ok; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.83..1.06
|
||||
- `TopsEdited` — ok; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.83..1.06
|
||||
- `coat` — ok; verts=1896, tris=0 degenerate, influences=4, unweighted=0, edge=0.83..0.95
|
||||
- `hair0001` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=0.95..1.18
|
||||
- `hair0002` — ok; verts=7022, tris=0 degenerate, influences=4, unweighted=0, edge=0.95..1.18
|
||||
- `hair0003` — ok; verts=7022, tris=0 degenerate, influences=0, unweighted=0, edge=1.00..1.00
|
||||
- `shoes1` — ok; verts=478, tris=0 degenerate, influences=3, unweighted=0, edge=0.93..0.99
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,660 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate first-pass age-band character seeds from the adult assets.
|
||||
|
||||
Run with Blender 3.6 batch mode, for example::
|
||||
|
||||
blender -b -P assets/blender/characters/generate_age_blends.py -- \
|
||||
--age elder --sex male --report-only
|
||||
|
||||
The script is an authoring tool: it writes committed seed ``.blend`` files and
|
||||
conversion reports. It is deliberately NOT part of the CMake build, and it
|
||||
will not overwrite a file that changed since it was generated unless
|
||||
``--force`` is given. See AGES-PLAN.md for the design.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
from agegen import anim, deform, profiles, report, rig, weights # noqa: E402
|
||||
|
||||
|
||||
def log(msg):
|
||||
print("[agegen] " + msg, flush=True)
|
||||
|
||||
|
||||
def sha256(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def manifest_path(out_dir):
|
||||
return os.path.join(out_dir, "AGE-GENERATION.json")
|
||||
|
||||
|
||||
def load_manifest(out_dir):
|
||||
path = manifest_path(out_dir)
|
||||
if not os.path.exists(path):
|
||||
return {"version": 1, "stems": {}}
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
if "stems" not in data:
|
||||
data["stems"] = {}
|
||||
return data
|
||||
except Exception:
|
||||
return {"version": 1, "stems": {}}
|
||||
|
||||
|
||||
def save_manifest(out_dir, data):
|
||||
data["profileHash"] = profiles.profile_hash()
|
||||
data["updated"] = int(time.time())
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
with open(manifest_path(out_dir), "w") as f:
|
||||
json.dump(data, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def find_anim_source(sex, explicit):
|
||||
if explicit:
|
||||
return explicit if os.path.exists(explicit) else None
|
||||
name = f"vrm-vroid-normal-{sex}.blend"
|
||||
roots = [
|
||||
os.getcwd(),
|
||||
os.path.dirname(_THIS_DIR),
|
||||
os.path.dirname(os.path.dirname(_THIS_DIR)),
|
||||
"/media/slapin/library/ogre3/ogre-projects/world2",
|
||||
]
|
||||
patterns = []
|
||||
for root in roots:
|
||||
patterns += [
|
||||
os.path.join(root, "build-vscode", "assets", "blender", name),
|
||||
os.path.join(root, "build", "assets", "blender", name),
|
||||
os.path.join(root, "assets", "blender", name),
|
||||
]
|
||||
patterns += glob.glob(
|
||||
os.path.join(os.getcwd(), "build*", "assets", "blender", name))
|
||||
for path in patterns:
|
||||
if path and os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def source_paths(age, sex, source_dir):
|
||||
stem = profiles.stem(age, sex)
|
||||
base = os.path.join(source_dir,
|
||||
f"edited-normal-{sex}.blend")
|
||||
clothes = {
|
||||
slot: os.path.join(source_dir, f"clothes-{sex}-{slot}.blend")
|
||||
for slot in ("top", "bottom", "feet", "hair")
|
||||
}
|
||||
out_base = os.path.join(source_dir,
|
||||
f"edited-normal-{age}-{sex}.blend")
|
||||
out_clothes = {
|
||||
slot: os.path.join(source_dir,
|
||||
f"clothes-{age}-{sex}-{slot}.blend")
|
||||
for slot in ("top", "bottom", "feet", "hair")
|
||||
}
|
||||
return stem, base, clothes, out_base, out_clothes
|
||||
|
||||
|
||||
def all_outputs(out_base, out_clothes):
|
||||
return [out_base] + [out_clothes[s] for s in
|
||||
("top", "bottom", "feet", "hair")]
|
||||
|
||||
|
||||
def stem_protected(stem, outputs, manifest, force):
|
||||
if force:
|
||||
return False
|
||||
entry = manifest.get("stems", {}).get(stem)
|
||||
if not entry:
|
||||
# Existing files without a manifest are assumed to be hand-authored.
|
||||
return any(os.path.exists(p) for p in outputs)
|
||||
if entry.get("state") == "hand-edited":
|
||||
return True
|
||||
hashes = entry.get("outputHashes", {})
|
||||
for path in outputs:
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
want = hashes.get(os.path.basename(path))
|
||||
if want and sha256(path) != want:
|
||||
log(f" {os.path.basename(path)} changed since generation; "
|
||||
f"treating as hand-edited")
|
||||
return True
|
||||
if not want:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def append_objects(blend_path, names):
|
||||
before = set(bpy.data.objects)
|
||||
with bpy.data.libraries.load(blend_path, link=False) as (src, dst):
|
||||
dst.objects = [n for n in names if n in src.objects]
|
||||
loaded = []
|
||||
for obj in dst.objects:
|
||||
if obj is None:
|
||||
continue
|
||||
try:
|
||||
bpy.context.collection.objects.link(obj)
|
||||
except RuntimeError:
|
||||
pass
|
||||
loaded.append(obj)
|
||||
# Blender may pull in child objects of an appended armature. Keep only
|
||||
# the explicitly requested objects; the conversion math does not need
|
||||
# their children, and clothes seed files must not contain body meshes.
|
||||
keep = set(loaded)
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj not in before and obj not in keep:
|
||||
try:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
except Exception:
|
||||
pass
|
||||
return loaded
|
||||
|
||||
|
||||
def delete_object(obj):
|
||||
if obj is None:
|
||||
return
|
||||
try:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def purge_orphans():
|
||||
for _ in range(3):
|
||||
try:
|
||||
bpy.ops.outliner.orphans_purge(
|
||||
do_local_ids=True, do_linked_ids=True,
|
||||
do_recursive=True)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
|
||||
def cleanup_seed_objects(adult_arm):
|
||||
"""Remove Blender control junk that must not live in a committed seed."""
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj == adult_arm:
|
||||
continue
|
||||
low = obj.name.lower()
|
||||
if obj.name.startswith("cs_") or "collider" in low:
|
||||
delete_object(obj)
|
||||
continue
|
||||
if obj.type == 'ARMATURE':
|
||||
# Keep only the age armature (and any hair armature handled by
|
||||
# the clothes converter). The adult armature is deleted by the
|
||||
# caller after animation retargeting.
|
||||
continue
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj.type == 'EMPTY' and obj.name.startswith("cs_"):
|
||||
delete_object(obj)
|
||||
|
||||
|
||||
def set_catalog_props(obj, age, sex):
|
||||
if not obj or obj.type != 'MESH':
|
||||
return
|
||||
if all(k in obj for k in ("age", "sex", "slot")):
|
||||
obj["age"] = profiles.normalize_age(age)
|
||||
obj["sex"] = sex
|
||||
obj["agegen_state"] = "auto-seed"
|
||||
obj["agegen_profile"] = profiles.profile_hash()
|
||||
|
||||
|
||||
def convert_body(age, sex, base_path, out_path, anim_source,
|
||||
write, report_only):
|
||||
log(f"loading adult base {base_path}")
|
||||
bpy.ops.wm.open_mainfile(filepath=base_path)
|
||||
bpy.context.scene.frame_set(1)
|
||||
|
||||
adult_arm = bpy.data.objects.get(sex)
|
||||
if adult_arm is None or adult_arm.type != 'ARMATURE':
|
||||
raise RuntimeError(f"adult armature '{sex}' not found in {base_path}")
|
||||
|
||||
prof = profiles.profile(age)
|
||||
stem = profiles.stem(age, sex)
|
||||
age_arm = rig.build_age_armature(adult_arm, age, sex, prof)
|
||||
log(f"built armature {age_arm.name}")
|
||||
|
||||
rest_data = rig.rest_transform_data(adult_arm, age_arm, age, prof)
|
||||
dmat = rig.dmatrices_from_data(rest_data)
|
||||
bad = rig.validate_rest_orientation(adult_arm, age_arm)
|
||||
if bad:
|
||||
raise RuntimeError("rest orientation changed for bones: " +
|
||||
", ".join(bad[:10]))
|
||||
|
||||
rep = report.ConversionReport(age, sex)
|
||||
rep.info["sourceBase"] = os.path.basename(base_path)
|
||||
rep.info["adultArmature"] = adult_arm.name
|
||||
rep.info["ageArmature"] = age_arm.name
|
||||
rep.rig = {
|
||||
"adultBones": len(adult_arm.data.bones),
|
||||
"ageBones": len(age_arm.data.bones),
|
||||
"restOrientationErrors": bad,
|
||||
"profile": prof,
|
||||
}
|
||||
|
||||
converted = []
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if obj.name.startswith("cs_"):
|
||||
continue
|
||||
if obj == adult_arm:
|
||||
continue
|
||||
if obj.data is None:
|
||||
continue
|
||||
if obj.vertex_groups:
|
||||
weights.limit_vertex_influences(obj, 4)
|
||||
metrics = deform.deform_mesh_object(
|
||||
obj, adult_arm, age_arm, dmat, prof, retarget_parent=True)
|
||||
if metrics is None:
|
||||
continue
|
||||
set_catalog_props(obj, age, sex)
|
||||
is_catalog = all(k in obj for k in ("age", "sex", "slot"))
|
||||
quality = report.evaluate_object_quality(metrics, is_catalog)
|
||||
obj["agegen_quality"] = quality
|
||||
rep.add_object(obj.name, metrics, quality=quality)
|
||||
if is_catalog:
|
||||
converted.append((obj, metrics))
|
||||
|
||||
seam = deform.snap_seams(converted)
|
||||
rep.seams = seam
|
||||
log(f"seam groups={seam['coincidentGroups']} "
|
||||
f"snapped={seam['snappedVertices']} maxGap={seam['maxGap']:.6f}")
|
||||
|
||||
# Required catalog slots per sex.
|
||||
slots = sorted({obj.get("slot") for obj, _ in converted
|
||||
if obj.get("slot")})
|
||||
rep.info["slots"] = ",".join(slots)
|
||||
for required in ("face", "top", "bottom", "feet", "hair"):
|
||||
if required not in slots:
|
||||
rep.info.setdefault("missingSlots", [])
|
||||
rep.info["missingSlots"].append(required)
|
||||
|
||||
if anim_source and os.path.exists(anim_source):
|
||||
try:
|
||||
summary = anim.append_and_retarget(
|
||||
age_arm, adult_arm, anim_source, age, sex)
|
||||
rep.info["animations"] = summary
|
||||
if summary.get("missing"):
|
||||
rep.info["missingAnimations"] = summary["missing"]
|
||||
except Exception as exc:
|
||||
rep.info["animationError"] = str(exc)
|
||||
log(f"animation retarget failed: {exc}")
|
||||
else:
|
||||
rep.info["animationError"] = "animation source not found"
|
||||
log("animation source not found; seed will have no clips")
|
||||
|
||||
# Remove the adult armature and control junk; re-parented meshes remain.
|
||||
cleanup_seed_objects(adult_arm)
|
||||
delete_object(adult_arm)
|
||||
|
||||
# Name and validate the single body armature.
|
||||
age_arm.name = stem
|
||||
age_arm.data.name = stem
|
||||
set_catalog_props(age_arm, age, sex)
|
||||
for obj in bpy.data.objects:
|
||||
set_catalog_props(obj, age, sex)
|
||||
|
||||
rep.info["outputBase"] = os.path.basename(out_path)
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
purge_orphans()
|
||||
if write:
|
||||
bpy.ops.wm.save_as_mainfile(filepath=out_path)
|
||||
log(f"saved body seed {out_path}")
|
||||
return rep, rest_data, age_arm
|
||||
|
||||
|
||||
def find_own_hair_armature(obj, adult_arm, age_arm):
|
||||
own_name = obj.get("own_armature_name", "")
|
||||
if own_name:
|
||||
arm = bpy.data.objects.get(own_name)
|
||||
if arm and arm.type == 'ARMATURE':
|
||||
return arm
|
||||
for mod in obj.modifiers:
|
||||
if mod.type != 'ARMATURE' or mod.object is None:
|
||||
continue
|
||||
if mod.object != adult_arm and mod.object != age_arm:
|
||||
return mod.object
|
||||
if obj.parent and obj.parent.type == 'ARMATURE' and \
|
||||
obj.parent not in (adult_arm, age_arm):
|
||||
return obj.parent
|
||||
return None
|
||||
|
||||
|
||||
def rename_hair_armature(arm, stem, original_name):
|
||||
safe = "".join(c if c.isalnum() or c in "-_" else "_"
|
||||
for c in original_name)
|
||||
new_name = f"{stem}-hair-{safe}"
|
||||
arm.name = new_name
|
||||
arm.data.name = new_name
|
||||
return new_name
|
||||
|
||||
|
||||
def transfer_weights_from_body(obj, body_obj):
|
||||
"""Copy body weight groups onto an unskinned garment/helper.
|
||||
|
||||
The source clothes files intentionally carry no rig; the normal build
|
||||
transfers weights later. Doing it before conversion gives the geometry a
|
||||
smooth deformation instead of nearest-bone rigid pieces, and the build
|
||||
repeats it from the converted body as usual.
|
||||
"""
|
||||
if body_obj is None or body_obj.type != 'MESH':
|
||||
return False
|
||||
if not body_obj.vertex_groups:
|
||||
return False
|
||||
obj.vertex_groups.clear()
|
||||
dt = obj.modifiers.new(name="WT", type='DATA_TRANSFER')
|
||||
dt.object = body_obj
|
||||
dt.use_vert_data = True
|
||||
dt.data_types_verts = {'VGROUP_WEIGHTS'}
|
||||
dt.layers_vgroup_select_src = 'ALL'
|
||||
dt.vert_mapping = 'POLYINTERP_NEAREST'
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
try:
|
||||
bpy.ops.object.datalayout_transfer(modifier=dt.name)
|
||||
bpy.ops.object.modifier_apply(modifier=dt.name)
|
||||
except Exception as exc:
|
||||
log(f" weight transfer failed for {obj.name}: {exc}")
|
||||
try:
|
||||
obj.modifiers.remove(dt)
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
weights.limit_vertex_influences(obj, 4)
|
||||
return True
|
||||
|
||||
|
||||
def convert_clothes(age, sex, slot, clothes_path, out_path,
|
||||
adult_base, body_seed, rest_data, write, rep,
|
||||
profile):
|
||||
log(f"converting {slot}: {clothes_path}")
|
||||
bpy.ops.wm.open_mainfile(filepath=clothes_path)
|
||||
stem = profiles.stem(age, sex)
|
||||
|
||||
adult_arms = append_objects(adult_base, [sex])
|
||||
age_arms = append_objects(body_seed, [stem])
|
||||
if not adult_arms or not age_arms:
|
||||
raise RuntimeError("failed to append adult/age armatures")
|
||||
adult_arm = adult_arms[0]
|
||||
age_arm = age_arms[0]
|
||||
dmat = rig.dmatrices_from_data(rest_data)
|
||||
prof = profile
|
||||
adult_head, age_head = rig.head_pivots(adult_arm, age_arm)
|
||||
head_scale = float(prof["head"])
|
||||
|
||||
ref_cache = {}
|
||||
|
||||
def get_ref(name):
|
||||
if not name:
|
||||
return None
|
||||
if name not in ref_cache:
|
||||
objs = append_objects(body_seed, [name])
|
||||
ref_cache[name] = objs[0] if objs else None
|
||||
return ref_cache[name]
|
||||
|
||||
hair_arm_map = {}
|
||||
meshes = [o for o in bpy.data.objects
|
||||
if o.type == 'MESH' and o.data is not None]
|
||||
for obj in meshes:
|
||||
own_arm = find_own_hair_armature(obj, adult_arm, age_arm)
|
||||
if own_arm is not None:
|
||||
if own_arm.name not in hair_arm_map:
|
||||
old = own_arm.name
|
||||
new = rename_hair_armature(own_arm, stem, old)
|
||||
hair_arm_map[own_arm.name] = new
|
||||
rig.scale_hair_armature(own_arm, adult_head, age_head,
|
||||
head_scale)
|
||||
log(f" scaled hair armature {old} -> {new}")
|
||||
deform.transform_hair_uniform(obj, adult_head, age_head,
|
||||
head_scale)
|
||||
obj["has_own_armature"] = True
|
||||
obj["own_armature_name"] = own_arm.name
|
||||
obj.parent = own_arm
|
||||
mod = next((m for m in obj.modifiers
|
||||
if m.type == 'ARMATURE'), None)
|
||||
if mod:
|
||||
mod.object = own_arm
|
||||
metrics = {
|
||||
"vertices": len(obj.data.vertices),
|
||||
"polygons": len(obj.data.polygons),
|
||||
"shapeKeys": 0,
|
||||
"unweighted": 0,
|
||||
"maxInfluences": 0,
|
||||
"edgeRatioMin": 1.0,
|
||||
"edgeRatioMax": 1.0,
|
||||
"degenerateTris": 0,
|
||||
}
|
||||
quality = "ok"
|
||||
else:
|
||||
ref = get_ref(obj.get("ref_clothing", ""))
|
||||
transfer_weights_from_body(obj, ref)
|
||||
metrics = deform.deform_mesh_object(
|
||||
obj, adult_arm, age_arm, dmat, prof,
|
||||
retarget_parent=False)
|
||||
quality = report.evaluate_object_quality(metrics, True) \
|
||||
if metrics else "warn"
|
||||
obj["agegen_quality"] = quality
|
||||
# Garment seed files carry no body armature.
|
||||
obj.parent = None
|
||||
for mod in list(obj.modifiers):
|
||||
if mod.type == 'ARMATURE':
|
||||
obj.modifiers.remove(mod)
|
||||
if "ref_layer" in obj:
|
||||
obj["ref_age"] = profiles.normalize_age(age)
|
||||
obj["ref_sex"] = sex
|
||||
obj["agegen_state"] = "auto-seed"
|
||||
rep.add_object(obj.name, metrics, quality=quality)
|
||||
|
||||
# Remove the appended body armatures and reference helpers, keep hair
|
||||
# armatures.
|
||||
for ref in ref_cache.values():
|
||||
delete_object(ref)
|
||||
for arm in age_arms + adult_arms:
|
||||
delete_object(arm)
|
||||
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
purge_orphans()
|
||||
if write:
|
||||
bpy.ops.wm.save_as_mainfile(filepath=out_path)
|
||||
log(f"saved clothes seed {out_path}")
|
||||
|
||||
|
||||
def check_seed(age, sex, source_dir):
|
||||
stem, base, clothes, out_base, out_clothes = source_paths(
|
||||
age, sex, source_dir)
|
||||
outputs = all_outputs(out_base, out_clothes)
|
||||
manifest = load_manifest(source_dir)
|
||||
entry = manifest.get("stems", {}).get(stem)
|
||||
missing = [p for p in outputs if not os.path.exists(p)]
|
||||
problems = []
|
||||
if missing:
|
||||
problems.append("missing: " + ", ".join(os.path.basename(p)
|
||||
for p in missing))
|
||||
if not entry:
|
||||
problems.append("no manifest entry")
|
||||
else:
|
||||
if entry.get("profileHash") not in (None,
|
||||
profiles.profile_hash()):
|
||||
problems.append("profile hash mismatch")
|
||||
hashes = entry.get("outputHashes", {})
|
||||
for p in outputs:
|
||||
if not os.path.exists(p):
|
||||
continue
|
||||
want = hashes.get(os.path.basename(p))
|
||||
if not want:
|
||||
problems.append("no hash for " + os.path.basename(p))
|
||||
elif sha256(p) != want:
|
||||
problems.append("changed: " + os.path.basename(p))
|
||||
if problems:
|
||||
log(f"CHECK FAIL {stem}: " + "; ".join(problems))
|
||||
return False
|
||||
log(f"CHECK OK {stem}")
|
||||
return True
|
||||
|
||||
|
||||
def process_pair(age, sex, args):
|
||||
stem, base, clothes, out_base, out_clothes = source_paths(
|
||||
age, sex, args.source_dir)
|
||||
outputs = all_outputs(out_base, out_clothes)
|
||||
manifest = load_manifest(args.source_dir)
|
||||
|
||||
if args.check:
|
||||
return check_seed(age, sex, args.source_dir)
|
||||
|
||||
if not os.path.exists(base):
|
||||
raise RuntimeError(f"adult base not found: {base}")
|
||||
if stem_protected(stem, outputs, manifest, args.force):
|
||||
log(f"SKIP {stem}: output exists and is hand-edited/changed "
|
||||
f"(use --force to overwrite)")
|
||||
return True
|
||||
|
||||
anim_source = find_anim_source(sex, args.anim_source)
|
||||
if anim_source:
|
||||
log(f"animation source: {anim_source}")
|
||||
else:
|
||||
log("WARNING: animation source not found; no clips will be added")
|
||||
|
||||
# Body conversion needs a file for the clothes pass even in report-only
|
||||
# mode; use a temp file that is removed afterwards.
|
||||
tmp_dir = tempfile.mkdtemp(prefix="agegen-")
|
||||
body_seed = out_base if (args.write and not args.report_only) else \
|
||||
os.path.join(tmp_dir, os.path.basename(out_base))
|
||||
|
||||
rep, rest_data, _age_arm = convert_body(
|
||||
age, sex, base, body_seed, anim_source,
|
||||
write=True, report_only=args.report_only)
|
||||
if args.report_only:
|
||||
log(f"temporary body seed: {body_seed}")
|
||||
|
||||
if not args.no_clothes:
|
||||
for slot in ("top", "bottom", "feet", "hair"):
|
||||
src = clothes[slot]
|
||||
if not os.path.exists(src):
|
||||
log(f"WARNING: missing clothes source {src}; skipping")
|
||||
continue
|
||||
convert_clothes(
|
||||
age, sex, slot, src, out_clothes[slot], base, body_seed,
|
||||
rest_data, write=(not args.report_only), rep=rep,
|
||||
profile=profiles.profile(age))
|
||||
|
||||
# Write report (always useful, even in report-only mode).
|
||||
report_path, report_json = rep.write(args.source_dir)
|
||||
log(f"report: {report_path}")
|
||||
|
||||
if args.report_only:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
return True
|
||||
|
||||
entry = {
|
||||
"state": "auto-seed",
|
||||
"age": profiles.normalize_age(age),
|
||||
"sex": sex,
|
||||
"profileHash": profiles.profile_hash(),
|
||||
"sourceHashes": {
|
||||
os.path.basename(base): sha256(base),
|
||||
**{os.path.basename(clothes[s]): sha256(clothes[s])
|
||||
for s in ("top", "bottom", "feet", "hair")
|
||||
if os.path.exists(clothes[s])},
|
||||
},
|
||||
"outputHashes": {
|
||||
os.path.basename(p): sha256(p)
|
||||
for p in outputs if os.path.exists(p)
|
||||
},
|
||||
"report": os.path.basename(report_path),
|
||||
"reportJson": os.path.basename(report_json),
|
||||
"updated": int(time.time()),
|
||||
}
|
||||
if anim_source:
|
||||
entry["animationSource"] = anim_source
|
||||
entry["animationSourceHash"] = sha256(anim_source)
|
||||
manifest.setdefault("stems", {})[stem] = entry
|
||||
save_manifest(args.source_dir, manifest)
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
if rep.status() == "fail" and not args.allow_fail:
|
||||
log(f"WARNING: {stem} has fail-level objects; see report")
|
||||
return True
|
||||
|
||||
|
||||
def parse_args():
|
||||
argv = sys.argv
|
||||
if "--" in argv:
|
||||
argv = argv[argv.index("--") + 1:]
|
||||
else:
|
||||
argv = []
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--age", choices=profiles.CANONICAL_AGES)
|
||||
p.add_argument("--sex", choices=["male", "female"])
|
||||
p.add_argument("--all", action="store_true")
|
||||
p.add_argument("--source-dir", default=_THIS_DIR)
|
||||
p.add_argument("--anim-source", default="")
|
||||
p.add_argument("--report-only", action="store_true")
|
||||
p.add_argument("--write", action="store_true")
|
||||
p.add_argument("--check", action="store_true")
|
||||
p.add_argument("--force", action="store_true")
|
||||
p.add_argument("--only-unedited", action="store_true")
|
||||
p.add_argument("--allow-fail", action="store_true")
|
||||
p.add_argument("--no-clothes", action="store_true")
|
||||
p.add_argument("--no-anim", action="store_true")
|
||||
args = p.parse_args(argv)
|
||||
if not args.check and not args.report_only and not args.write:
|
||||
args.write = True
|
||||
if args.only_unedited:
|
||||
args.write = True
|
||||
if args.all:
|
||||
if args.age or args.sex:
|
||||
p.error("--all cannot be combined with --age/--sex")
|
||||
else:
|
||||
if not args.age or not args.sex:
|
||||
p.error("specify --age and --sex, or --all")
|
||||
if args.age == "adult":
|
||||
p.error("adult is the source, not a generated age")
|
||||
if args.no_anim:
|
||||
args.anim_source = "__none__"
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
args.source_dir = os.path.abspath(args.source_dir)
|
||||
pairs = []
|
||||
if args.all:
|
||||
for age in ("elder", "teenager", "child", "baby"):
|
||||
for sex in ("male", "female"):
|
||||
pairs.append((age, sex))
|
||||
else:
|
||||
pairs.append((profiles.normalize_age(args.age), args.sex))
|
||||
ok = True
|
||||
for age, sex in pairs:
|
||||
log(f"=== {age} / {sex} ===")
|
||||
try:
|
||||
if not process_pair(age, sex, args):
|
||||
ok = False
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
log(f"FAILED {age}/{sex}: {exc}")
|
||||
ok = False
|
||||
if not ok:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,18 +2,18 @@
|
||||
#
|
||||
# Run in Blender batch mode with the consolidated blend already loaded:
|
||||
#
|
||||
# blender -b -Y edited-normal-<sex>-consolidated.blend \
|
||||
# -P generate_lods.py -- <sex> <out_dir> <lods_blend_out>
|
||||
# blender -b -Y <consolidated.blend> \
|
||||
# -P generate_lods.py -- <age> <sex> <out_dir> <lods_blend_out>
|
||||
#
|
||||
# Produces, per sex:
|
||||
# * <out_dir>/charlod_<sex>-atlas.png - shared 2048x2048 LOD atlas
|
||||
# * <out_dir>/charlod_<sex>.json - manifest
|
||||
# Produces, per (age, sex) stem (adult keeps the legacy sex stem):
|
||||
# * <out_dir>/charlod_<stem>-atlas.png - shared 2048x2048 LOD atlas
|
||||
# * <out_dir>/charlod_<stem>.json - manifest
|
||||
# * LOD1 object variants `<part>_lod1` - same geometry, no shape keys,
|
||||
# UVs remapped into the atlas, one shared material `charlod_<sex>`
|
||||
# UVs remapped into the atlas, one shared material `charlod_<stem>`
|
||||
# (hair slot is skipped - hair is hidden/impostor-baked at distance)
|
||||
# * LOD2 impostors `charlod_<sex>_{thin,fat}_lod2` - the deterministic
|
||||
# * LOD2 impostors `charlod_<stem>_{thin,fat}_lod2` - the deterministic
|
||||
# default outfit joined into one mesh skinned to the body armature
|
||||
# * LOD3 impostors `charlod_<sex>_{thin,fat}_lod3` - decimate of LOD2,
|
||||
# * LOD3 impostors `charlod_<stem>_{thin,fat}_lod3` - decimate of LOD2,
|
||||
# <= 400 triangles (hard limit)
|
||||
# * saves everything into <lods_blend_out>
|
||||
#
|
||||
@@ -52,6 +52,11 @@ import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
import age_profiles
|
||||
|
||||
ATLAS_SIZE = 2048
|
||||
LOD3_MAX_TRIS = 400
|
||||
REQUIRED_PROPS = {"age", "sex", "slot"}
|
||||
@@ -89,11 +94,14 @@ def log(msg):
|
||||
def parse_args():
|
||||
argv = sys.argv
|
||||
argv = argv[argv.index("--") + 1:]
|
||||
if len(argv) < 3:
|
||||
if len(argv) < 4:
|
||||
raise RuntimeError(
|
||||
"usage: blender -b -Y <consolidated.blend> -P generate_lods.py"
|
||||
" -- <sex> <out_dir> <lods_blend_out>")
|
||||
return argv[0], argv[1], argv[2]
|
||||
" -- <age> <sex> <out_dir> <lods_blend_out>")
|
||||
age = age_profiles.normalize_age(argv[0])
|
||||
sex = argv[1]
|
||||
stem = age_profiles.stem(age, sex)
|
||||
return stem, argv[2], argv[3]
|
||||
|
||||
|
||||
def strip_suffix(name):
|
||||
@@ -617,12 +625,13 @@ def write_manifest(sex, out_dir):
|
||||
|
||||
def main():
|
||||
sex, out_dir, lods_blend_out = parse_args()
|
||||
log("sex=%s out_dir=%s lods_blend=%s" % (sex, out_dir,
|
||||
lods_blend_out))
|
||||
stem = sex
|
||||
log("stem=%s out_dir=%s lods_blend=%s" % (stem, out_dir,
|
||||
lods_blend_out))
|
||||
|
||||
cleanup_previous(sex)
|
||||
cleanup_previous(stem)
|
||||
|
||||
armature = find_armature(sex)
|
||||
armature = find_armature(stem)
|
||||
head_bone = find_head_bone(armature)
|
||||
log("shared armature '%s', head bone '%s'"
|
||||
% (armature.name, head_bone))
|
||||
@@ -632,22 +641,22 @@ def main():
|
||||
% (len(catalog), [ob.name for ob in catalog]))
|
||||
|
||||
log("=== step 1: atlas ===")
|
||||
atlas, rects = build_atlas(catalog, sex, out_dir)
|
||||
atlas_mat = make_atlas_material(sex, atlas)
|
||||
atlas, rects = build_atlas(catalog, stem, out_dir)
|
||||
atlas_mat = make_atlas_material(stem, atlas)
|
||||
|
||||
log("=== step 2: LOD1 variants ===")
|
||||
lod1 = generate_lod1(catalog, rects, atlas_mat, sex)
|
||||
lod1 = generate_lod1(catalog, rects, atlas_mat, stem)
|
||||
log("generated %d LOD1 objects" % len(lod1))
|
||||
|
||||
log("=== step 3: LOD2 impostors ===")
|
||||
lod2 = generate_lod2(catalog, rects, atlas_mat, armature,
|
||||
head_bone, sex)
|
||||
head_bone, stem)
|
||||
|
||||
log("=== step 4: LOD3 impostors ===")
|
||||
generate_lod3(lod2, armature, sex)
|
||||
generate_lod3(lod2, armature, stem)
|
||||
|
||||
log("=== step 5: manifest + save ===")
|
||||
write_manifest(sex, out_dir)
|
||||
write_manifest(stem, out_dir)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=lods_blend_out)
|
||||
log("saved lods blend to %s" % lods_blend_out)
|
||||
log("DONE")
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed an optional ``nude-<stem>.blend`` from the existing layer-0 parts.
|
||||
|
||||
``nude-<stem>.blend`` holds the optional anatomically detailed body parts
|
||||
for one age/sex band. This helper makes the first version for a stem by
|
||||
copying the current composition bases (``BodyTop`` / ``BodyBottom`` /
|
||||
``BodyFeet``) into a standalone blend, tagging them ``detailed-nude`` and
|
||||
giving them unique object/data names. Edit the resulting file in Blender;
|
||||
the normal character pipeline appends it after all clothing stages and the
|
||||
runtime prefers those parts whenever a slot has no layer-1/2 garment.
|
||||
|
||||
Run through CMake (recommended; participates in the Blender lock)::
|
||||
|
||||
cmake --build build-vscode --target init-nude-male
|
||||
|
||||
or directly::
|
||||
|
||||
blender -b -Y -P assets/blender/characters/make_nude_blend.py -- \
|
||||
--age adult --sex male [--force]
|
||||
|
||||
The helper never overwrites an existing file without ``--force``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
import age_profiles # noqa: E402
|
||||
import add_missing_shape_keys as shape_keys # noqa: E402
|
||||
|
||||
SLOT_ORDER = ("top", "bottom", "feet")
|
||||
BASE_NAMES = {
|
||||
"top": "BodyTop",
|
||||
"bottom": "BodyBottom",
|
||||
"feet": "BodyFeet",
|
||||
}
|
||||
CLOTHING_PROPS = (
|
||||
"garments",
|
||||
"_combined_meta",
|
||||
"ref_layer",
|
||||
"ref_part",
|
||||
"garment_id",
|
||||
"ref_age",
|
||||
"ref_sex",
|
||||
"ref_clothing",
|
||||
"tags",
|
||||
"own_skeleton",
|
||||
"has_own_armature",
|
||||
"own_armature_name",
|
||||
)
|
||||
|
||||
|
||||
def log(msg):
|
||||
print("[nude-seed] " + msg, flush=True)
|
||||
|
||||
|
||||
def parse_args():
|
||||
argv = sys.argv
|
||||
if "--" in argv:
|
||||
argv = argv[argv.index("--") + 1:]
|
||||
else:
|
||||
argv = []
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--age", required=True,
|
||||
choices=age_profiles.CANONICAL_AGES)
|
||||
p.add_argument("--sex", required=True, choices=("male", "female"))
|
||||
p.add_argument("--source", default="")
|
||||
p.add_argument("--output", default="")
|
||||
p.add_argument("--slots", default=",".join(SLOT_ORDER))
|
||||
p.add_argument("--force", action="store_true")
|
||||
return p.parse_args(argv)
|
||||
|
||||
|
||||
def object_name_score(obj):
|
||||
"""Prefer the canonical base name, then the shortest name.
|
||||
|
||||
Mirrors CharacterSlotSystem's layer-0 fallback: Blender duplicate
|
||||
suffixes are penalised so ``BodyTop`` beats ``BodyTop.001``.
|
||||
"""
|
||||
name = obj.name
|
||||
score = len(name)
|
||||
if name.startswith(BASE_NAMES.get(obj.get("slot", ""), "\0")):
|
||||
score -= 1000
|
||||
if "." in name:
|
||||
score += 1000
|
||||
return score
|
||||
|
||||
|
||||
def find_body_armature(stem):
|
||||
arm = bpy.data.objects.get(stem)
|
||||
if arm is not None and arm.type == 'ARMATURE':
|
||||
return arm
|
||||
candidates = [o for o in bpy.data.objects if o.type == 'ARMATURE']
|
||||
if not candidates:
|
||||
return None
|
||||
return max(candidates, key=lambda o: len(o.data.bones))
|
||||
|
||||
|
||||
def find_base_part(slot, age, sex):
|
||||
candidates = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH' or obj.data is None:
|
||||
continue
|
||||
if not all(k in obj for k in ("age", "sex", "slot")):
|
||||
continue
|
||||
if age_profiles.normalize_age(obj.get("age", "adult")) != age:
|
||||
continue
|
||||
if str(obj.get("sex", "")) != sex:
|
||||
continue
|
||||
if str(obj.get("slot", "")) != slot:
|
||||
continue
|
||||
if int(obj.get("layer", 0)) != 0:
|
||||
continue
|
||||
candidates.append(obj)
|
||||
if not candidates:
|
||||
return None
|
||||
return min(candidates, key=object_name_score)
|
||||
|
||||
|
||||
def find_shape_source():
|
||||
"""Find the body reference mesh that carries the full shape-key set."""
|
||||
for name in ("Body_shapes", "Body"):
|
||||
obj = bpy.data.objects.get(name)
|
||||
if obj is not None and obj.type == 'MESH' and \
|
||||
obj.data.shape_keys is not None and \
|
||||
len(obj.data.shape_keys.key_blocks) > 1:
|
||||
return obj
|
||||
return None
|
||||
|
||||
|
||||
def build_shape_data(source_obj):
|
||||
"""Same payload add_missing_shape_keys.py builds for its propagation."""
|
||||
return {
|
||||
"names": [kb.name for kb in source_obj.data.shape_keys.key_blocks],
|
||||
"vertex_positions": {
|
||||
sk.name: [(v.co.x, v.co.y, v.co.z) for v in sk.data]
|
||||
for sk in source_obj.data.shape_keys.key_blocks
|
||||
},
|
||||
"polygons": [[v for v in poly.vertices]
|
||||
for poly in source_obj.data.polygons],
|
||||
"is_relative": source_obj.data.shape_keys.use_relative,
|
||||
"source_obj_name": source_obj.name,
|
||||
}
|
||||
|
||||
|
||||
def duplicate_part(src, slot, age, sex, armature):
|
||||
dup = src.copy()
|
||||
dup.data = src.data.copy()
|
||||
bpy.context.collection.objects.link(dup)
|
||||
dup.name = src.name + "_detailed"
|
||||
dup.data.name = dup.name
|
||||
|
||||
# Keep the source transform (the base parts are usually parented to the
|
||||
# armature with an identity object matrix).
|
||||
dup.parent = armature
|
||||
dup.matrix_parent_inverse = src.matrix_parent_inverse.copy()
|
||||
dup.matrix_world = src.matrix_world.copy()
|
||||
|
||||
for prop in CLOTHING_PROPS:
|
||||
if prop in dup:
|
||||
del dup[prop]
|
||||
dup["age"] = age
|
||||
dup["sex"] = sex
|
||||
dup["slot"] = slot
|
||||
dup["layer"] = 0
|
||||
dup["clothing_tags"] = "detailed-nude"
|
||||
|
||||
mod = next((m for m in dup.modifiers if m.type == 'ARMATURE'), None)
|
||||
if mod is None:
|
||||
mod = dup.modifiers.new(name="Armature", type='ARMATURE')
|
||||
mod.object = armature
|
||||
|
||||
if not dup.vertex_groups:
|
||||
log(f"WARNING: '{dup.name}' has no vertex groups; skin it before "
|
||||
f"building")
|
||||
if dup.data.shape_keys is None:
|
||||
log(f"WARNING: '{dup.name}' has no shape keys; copy them from "
|
||||
f"the base part or use transfer_shape_keys.py")
|
||||
return dup
|
||||
|
||||
|
||||
def purge_orphans():
|
||||
for _ in range(3):
|
||||
try:
|
||||
bpy.ops.outliner.orphans_purge(
|
||||
do_local_ids=True, do_linked_ids=True,
|
||||
do_recursive=True)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
age = age_profiles.normalize_age(args.age)
|
||||
sex = args.sex
|
||||
stem = age_profiles.stem(age, sex)
|
||||
source = args.source or os.path.join(
|
||||
_THIS_DIR, f"edited-normal-{stem}.blend")
|
||||
output = args.output or os.path.join(_THIS_DIR, f"nude-{stem}.blend")
|
||||
|
||||
if not os.path.exists(source):
|
||||
log(f"ERROR: source body blend not found: {source}")
|
||||
return 1
|
||||
if os.path.exists(output) and not args.force:
|
||||
log(f"SKIP: {output} already exists (use --force to reseed)")
|
||||
return 0
|
||||
|
||||
slots = [s.strip() for s in args.slots.split(",") if s.strip()]
|
||||
unknown = [s for s in slots if s not in SLOT_ORDER]
|
||||
if unknown:
|
||||
log(f"ERROR: unknown slots: {', '.join(unknown)}")
|
||||
return 1
|
||||
|
||||
log(f"opening {source}")
|
||||
bpy.ops.wm.open_mainfile(filepath=source)
|
||||
armature = find_body_armature(stem)
|
||||
if armature is None:
|
||||
log(f"ERROR: no body armature found in {source}")
|
||||
return 1
|
||||
log(f"armature: {armature.name} ({len(armature.data.bones)} bones)")
|
||||
|
||||
shape_source = find_shape_source()
|
||||
shape_data = build_shape_data(shape_source) if shape_source else None
|
||||
if shape_source:
|
||||
log(f"shape source: {shape_source.name} "
|
||||
f"({len(shape_data['names'])} keys)")
|
||||
else:
|
||||
log("WARNING: no Body_shapes/Body shape-key source; the detailed "
|
||||
"parts will only get keys already present on the base part")
|
||||
|
||||
duplicates = []
|
||||
for slot in slots:
|
||||
src = find_base_part(slot, age, sex)
|
||||
if src is None:
|
||||
log(f"WARNING: no layer-0 {slot} part found; skipping")
|
||||
continue
|
||||
dup = duplicate_part(src, slot, age, sex, armature)
|
||||
if shape_data:
|
||||
try:
|
||||
shape_keys.add_missing_shape_keys(shape_data, dup)
|
||||
except Exception as exc:
|
||||
log(f"WARNING: shape-key propagation failed for "
|
||||
f"{dup.name}: {exc}")
|
||||
duplicates.append(dup)
|
||||
log(f"{slot}: {src.name} -> {dup.name} "
|
||||
f"({len(dup.data.vertices)} verts)")
|
||||
|
||||
if not duplicates:
|
||||
log("ERROR: no layer-0 parts found for the requested slots")
|
||||
return 1
|
||||
|
||||
# Keep exactly the new parts and the body armature. The user can model
|
||||
# the detailed variants in this file without the rest of the body.
|
||||
keep = set(duplicates) | {armature}
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj not in keep:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# A source-blend armature may carry the full animation library; the
|
||||
# detailed-nude file only needs the rest skeleton for skinning/editing.
|
||||
armature.animation_data_clear()
|
||||
for action in list(bpy.data.actions):
|
||||
try:
|
||||
bpy.data.actions.remove(action, do_unlink=True)
|
||||
except TypeError:
|
||||
bpy.data.actions.remove(action)
|
||||
except Exception:
|
||||
pass
|
||||
purge_orphans()
|
||||
|
||||
os.makedirs(os.path.dirname(output), exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=output)
|
||||
log(f"saved {output}")
|
||||
log("next: edit the *_detailed parts, then rebuild "
|
||||
f"character-{stem} (or the full build)")
|
||||
log("requirements: same armature/bind pose, max 4 bone influences, "
|
||||
"shape keys, matching boundary loops")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
@@ -2,6 +2,11 @@ import bpy
|
||||
import os
|
||||
import sys
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
import age_profiles
|
||||
|
||||
def clean_scene():
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete()
|
||||
@@ -36,8 +41,10 @@ def process_batch():
|
||||
try:
|
||||
args = sys.argv[sys.argv.index("--") + 1:]
|
||||
clothing_path, lib_directory, out_dir = args[0], args[1], args[2]
|
||||
explicit_body = args[3] if len(args) >= 4 else ""
|
||||
except (IndexError, ValueError):
|
||||
print("Usage: blender -b -P script.py -- <source_blend> <lib_dir> <out_dir>")
|
||||
print("Usage: blender -b -P script.py -- "
|
||||
"<source_blend> <lib_dir> <out_dir> [body_blend]")
|
||||
return
|
||||
|
||||
if not os.path.exists(out_dir): os.makedirs(out_dir)
|
||||
@@ -62,7 +69,7 @@ def process_batch():
|
||||
|
||||
# Get properties
|
||||
sex = clothing.get("ref_sex")
|
||||
age = clothing.get("ref_age")
|
||||
age = age_profiles.normalize_age(clothing.get("ref_age"))
|
||||
ref_mesh_name = clothing.get("ref_clothing")
|
||||
|
||||
if not all([sex, age, ref_mesh_name]):
|
||||
@@ -70,19 +77,22 @@ def process_batch():
|
||||
continue
|
||||
|
||||
# 3. Locate Reference Library
|
||||
target_lib_name = f"normal_{age}_{sex}.blend"
|
||||
target_lib_path = os.path.join(lib_directory, target_lib_name)
|
||||
rig_name = str(sex)
|
||||
|
||||
if not os.path.exists(target_lib_path):
|
||||
if target_lib_name == "normal_adult_male.blend":
|
||||
target_lib_name = "edited-normal-male.blend"
|
||||
elif target_lib_name == "normal_adult_female.blend":
|
||||
target_lib_name = "edited-normal-female.blend"
|
||||
rig_name = age_profiles.armature_name(age, sex)
|
||||
if explicit_body and os.path.exists(explicit_body):
|
||||
target_lib_path = explicit_body
|
||||
else:
|
||||
target_lib_name = f"normal_{age}_{sex}.blend"
|
||||
target_lib_path = os.path.join(lib_directory, target_lib_name)
|
||||
if not os.path.exists(target_lib_path):
|
||||
print(f"Error: Library {target_lib_path} not found")
|
||||
continue
|
||||
if age == "adult":
|
||||
fallback = {
|
||||
"male": "edited-normal-male.blend",
|
||||
"female": "edited-normal-female.blend",
|
||||
}.get(sex, "")
|
||||
target_lib_path = os.path.join(lib_directory, fallback)
|
||||
if not os.path.exists(target_lib_path):
|
||||
print(f"Error: Library {target_lib_path} not found")
|
||||
continue
|
||||
|
||||
# 4. Append Weights Source and Rig
|
||||
with bpy.data.libraries.load(target_lib_path) as (data_from, data_to):
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural validator for generated character age assets.
|
||||
|
||||
Run after the character build (the CMake target ``character-assets-check``
|
||||
does this automatically)::
|
||||
|
||||
python3 assets/blender/characters/validate_age_assets.py \
|
||||
--characters-dir build-vscode/characters
|
||||
|
||||
Checks:
|
||||
* every age/sex stem has the expected body skeleton and catalog slots;
|
||||
* catalog ages are canonical and clothing layers are present;
|
||||
* no duplicate resource basenames across the flat Characters namespace;
|
||||
* LOD atlas/material files exist;
|
||||
* material texture references resolve to a staged texture (warning only).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, THIS_DIR)
|
||||
import age_profiles
|
||||
|
||||
|
||||
def fail(msg, errors):
|
||||
errors.append(msg)
|
||||
print("FAIL: " + msg)
|
||||
|
||||
|
||||
def warn(msg, warnings):
|
||||
warnings.append(msg)
|
||||
print("WARN: " + msg)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--characters-dir", required=True)
|
||||
p.add_argument("--source-dir", default=THIS_DIR,
|
||||
help="character source directory containing optional "
|
||||
"nude-<stem>.blend files (default: %(default)s)")
|
||||
p.add_argument("--ages", default="",
|
||||
help="semicolon/comma separated age bands to check "
|
||||
"(default: all canonical bands)")
|
||||
args = p.parse_args()
|
||||
root = os.path.abspath(args.characters_dir)
|
||||
errors = []
|
||||
warnings = []
|
||||
if not os.path.isdir(root):
|
||||
print("FAIL: characters dir not found: " + root)
|
||||
return 1
|
||||
|
||||
dirs = sorted(d for d in os.listdir(root)
|
||||
if os.path.isdir(os.path.join(root, d)))
|
||||
by_stem = {}
|
||||
for d in dirs:
|
||||
by_stem[d] = os.path.join(root, d)
|
||||
|
||||
ages = age_profiles.CANONICAL_AGES
|
||||
if args.ages:
|
||||
ages = [a.strip() for a in re.split(r"[;,]", args.ages)
|
||||
if a.strip()]
|
||||
expected_stems = []
|
||||
for age in ages:
|
||||
for sex in ("male", "female"):
|
||||
expected_stems.append(age_profiles.stem(age, sex))
|
||||
|
||||
# Catch misspelled detailed-nude source names (e.g. nude-adult-male.blend)
|
||||
# instead of silently ignoring them.
|
||||
all_stems = {age_profiles.stem(age, sex)
|
||||
for age in age_profiles.CANONICAL_AGES
|
||||
for sex in ("male", "female")}
|
||||
source_root = os.path.abspath(args.source_dir)
|
||||
for path in sorted(glob.glob(os.path.join(source_root, "nude-*.blend"))):
|
||||
name = os.path.basename(path)
|
||||
stem = name[len("nude-"):-len(".blend")]
|
||||
if stem not in all_stems:
|
||||
warn(f"unexpected detailed-nude source name: {name} "
|
||||
f"(expected nude-<stem>.blend, e.g. nude-male.blend)",
|
||||
warnings)
|
||||
|
||||
for stem in expected_stems:
|
||||
d = by_stem.get(stem)
|
||||
if d is None:
|
||||
fail(f"missing output directory: {stem}", errors)
|
||||
continue
|
||||
cat_files = sorted(glob.glob(os.path.join(d, "body_part_*.json")))
|
||||
if not cat_files:
|
||||
fail(f"no body_part_*.json in {stem}", errors)
|
||||
continue
|
||||
entries = []
|
||||
for f in cat_files:
|
||||
try:
|
||||
with open(f) as fh:
|
||||
e = json.load(fh)
|
||||
except Exception as exc:
|
||||
fail(f"unreadable catalog {f}: {exc}", errors)
|
||||
continue
|
||||
entries.append(e)
|
||||
ages = {age_profiles.normalize_age(e.get("age", ""))
|
||||
for e in entries}
|
||||
stem_age = stem.split("-")[0] if "-" in stem else "adult"
|
||||
stem_sex = stem.rsplit("-", 1)[-1]
|
||||
if ages != {stem_age}:
|
||||
fail(f"{stem}: catalog ages {sorted(ages)} do not match stem",
|
||||
errors)
|
||||
slots = {}
|
||||
for e in entries:
|
||||
slots.setdefault(e.get("slot", ""), []).append(e)
|
||||
for req in ("face", "top", "bottom", "feet"):
|
||||
if req not in slots:
|
||||
fail(f"{stem}: missing required slot '{req}'", errors)
|
||||
for req in ("top", "bottom", "feet"):
|
||||
if not any(int(e.get("layer", 0)) == 2
|
||||
for e in slots.get(req, [])):
|
||||
fail(f"{stem}: no layer-2 {req} garment", errors)
|
||||
if "hair" not in slots:
|
||||
warn(f"{stem}: no hair slot (bald band)", warnings)
|
||||
|
||||
# Optional detailed-nude source file: when present, the built
|
||||
# catalog must expose its layer-0 parts. Missing slots are only a
|
||||
# warning because a nude-<stem>.blend may be populated gradually.
|
||||
nude_path = os.path.join(os.path.abspath(args.source_dir),
|
||||
f"nude-{stem}.blend")
|
||||
if os.path.exists(nude_path):
|
||||
any_detailed = False
|
||||
for req in ("top", "bottom", "feet"):
|
||||
detailed = []
|
||||
for e in slots.get(req, []):
|
||||
tags = e.get("tags", [])
|
||||
if isinstance(tags, str):
|
||||
tags = [t.strip() for t in tags.split(";")
|
||||
if t.strip()]
|
||||
if "detailed-nude" in tags:
|
||||
detailed.append(e)
|
||||
if not detailed:
|
||||
warn(f"{stem}: nude-{stem}.blend has no "
|
||||
f"detailed-nude {req} catalog entry (slot falls "
|
||||
f"back to the composition base)", warnings)
|
||||
continue
|
||||
any_detailed = True
|
||||
if len(detailed) > 1:
|
||||
fail(f"{stem}: multiple detailed-nude {req} entries: "
|
||||
+ ", ".join(e.get("mesh", "?")
|
||||
for e in detailed), errors)
|
||||
base_keys = set()
|
||||
for e in slots.get(req, []):
|
||||
tags = e.get("tags", [])
|
||||
if isinstance(tags, str):
|
||||
tags = [t.strip() for t in tags.split(";")
|
||||
if t.strip()]
|
||||
if int(e.get("layer", 0)) == 0 and \
|
||||
"detailed-nude" not in tags:
|
||||
base_keys.update(e.get("shape_keys", []))
|
||||
for e in detailed:
|
||||
mesh = e.get("mesh", "?")
|
||||
if int(e.get("layer", 0)) != 0:
|
||||
fail(f"{stem}: detailed-nude {req} is not layer 0: "
|
||||
f"{mesh}", errors)
|
||||
if e.get("garments"):
|
||||
fail(f"{stem}: detailed-nude {req} carries "
|
||||
f"garments: {mesh}", errors)
|
||||
if age_profiles.normalize_age(
|
||||
e.get("age", "")) != stem_age:
|
||||
fail(f"{stem}: detailed-nude {req} age mismatch: "
|
||||
f"{mesh}", errors)
|
||||
if str(e.get("sex", "")) != stem_sex:
|
||||
fail(f"{stem}: detailed-nude {req} sex mismatch: "
|
||||
f"{mesh}", errors)
|
||||
missing_keys = base_keys - set(
|
||||
e.get("shape_keys", []))
|
||||
if missing_keys:
|
||||
warn(f"{stem}: detailed-nude {req} is missing "
|
||||
f"shape keys {sorted(missing_keys)}: {mesh}",
|
||||
warnings)
|
||||
if not any_detailed:
|
||||
fail(f"{stem}: nude-{stem}.blend exists but the built "
|
||||
f"catalog has no detailed-nude entries "
|
||||
f"(re-run CMake and the character target)", errors)
|
||||
|
||||
skel = os.path.join(d, stem + ".skeleton")
|
||||
if not os.path.exists(skel):
|
||||
fail(f"{stem}: missing body skeleton {stem}.skeleton", errors)
|
||||
|
||||
atlas = os.path.join(d, f"charlod_{stem}-atlas.png")
|
||||
if not os.path.exists(atlas):
|
||||
fail(f"{stem}: missing LOD atlas charlod_{stem}-atlas.png",
|
||||
errors)
|
||||
mat = os.path.join(d, f"charlod_{stem}.material")
|
||||
if not os.path.exists(mat):
|
||||
warn(f"{stem}: missing LOD material charlod_{stem}.material",
|
||||
warnings)
|
||||
|
||||
# Resource basename uniqueness across the flat Characters namespace.
|
||||
seen = {}
|
||||
exts = (".mesh", ".skeleton", ".material", ".png", ".json")
|
||||
for d in dirs:
|
||||
for f in os.listdir(os.path.join(root, d)):
|
||||
if not f.endswith(exts):
|
||||
continue
|
||||
if f.startswith("OgreXMLConverter") or f.startswith(
|
||||
"OgreMeshUpgrader"):
|
||||
continue
|
||||
seen.setdefault(f, []).append(d)
|
||||
for name, dirs_with in sorted(seen.items()):
|
||||
if len(dirs_with) > 1:
|
||||
fail("duplicate resource basename '%s' in: %s" %
|
||||
(name, ", ".join(dirs_with)), errors)
|
||||
|
||||
# Material texture references: collect staged texture names.
|
||||
textures = set()
|
||||
for d in dirs:
|
||||
for f in os.listdir(os.path.join(root, d)):
|
||||
if f.lower().endswith((".png", ".jpg", ".jpeg", ".tga",
|
||||
".dds")):
|
||||
textures.add(f)
|
||||
for d in dirs:
|
||||
for f in glob.glob(os.path.join(root, d, "*.material")):
|
||||
with open(f, errors="ignore") as fh:
|
||||
for line in fh:
|
||||
m = re.match(r"\s*texture\s+(\S+)", line)
|
||||
if not m:
|
||||
continue
|
||||
tex = m.group(1)
|
||||
if tex not in textures:
|
||||
warn(f"material {os.path.basename(f)} references "
|
||||
f"missing texture {tex}", warnings)
|
||||
|
||||
print()
|
||||
print(f"checked {len(expected_stems)} stems in {root}")
|
||||
print(f"errors: {len(errors)}, warnings: {len(warnings)}")
|
||||
if errors:
|
||||
print("RESULT: FAIL")
|
||||
return 1
|
||||
print("RESULT: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -33,6 +33,18 @@ def prepare_armature(mapping):
|
||||
orig_armature = bpy.data.objects[mapping.armature_name]
|
||||
armature_name = orig_armature.name
|
||||
orig_armature.name = orig_armature.name + "_orig"
|
||||
# Rename the original data block too, so the baked armature can use the
|
||||
# exact stem as its data name (and therefore the exported skeleton file
|
||||
# is `<stem>.skeleton`, not `<stem>.005.skeleton`).
|
||||
orig_armature.data.name = armature_name + "_orig"
|
||||
# Orphaned armature data blocks left by removed duplicate clothing-stage
|
||||
# rigs still occupy the stem name; move them out of the way.
|
||||
for ad in bpy.data.armatures:
|
||||
if ad is orig_armature.data or ad is new_armature.data:
|
||||
continue
|
||||
if ad.name == armature_name or ad.name.startswith(
|
||||
armature_name + "."):
|
||||
ad.name = ad.name + "_unused"
|
||||
new_armature.name = armature_name
|
||||
queue = []
|
||||
if new_armature.animation_data is None:
|
||||
@@ -276,6 +288,19 @@ for mapping in[CommandLineMapping()]:
|
||||
else:
|
||||
print(f"No hair with own skeleton detected")
|
||||
# ---------------------------------------------
|
||||
# Helper/reference meshes (Body, Tops, Bottoms, Shoes, HairBack,
|
||||
# Accessory, Body_shapes) are build-time weight-transfer aids and
|
||||
# must not be exported: they are not catalog parts, and exporting
|
||||
# them bloats the resource tree and creates texture-name collisions
|
||||
# across age directories.
|
||||
allowed = set(mapping.objs)
|
||||
allowed.update(o.name for o in hair_own_skel_objs)
|
||||
for ob in list(bpy.data.objects):
|
||||
if ob.type != 'MESH':
|
||||
continue
|
||||
if ob.name not in allowed:
|
||||
print(f"Removing non-catalog helper mesh '{ob.name}'")
|
||||
bpy.data.objects.remove(ob, do_unlink=True)
|
||||
else:
|
||||
bpy.ops.wm.append(
|
||||
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
|
||||
@@ -390,8 +415,46 @@ for mapping in[CommandLineMapping()]:
|
||||
hair_arm.hide_render = False
|
||||
obj_names = mapping.objs
|
||||
prefix = mapping.armature_name + "_"
|
||||
material_obj_names = list(obj_names)
|
||||
material_obj_names += [o.name for o in hair_own_skel_objs]
|
||||
|
||||
for name in obj_names:
|
||||
# Deduplicate image datablocks that came from multiple clothing stages
|
||||
# but point at the same source file. Without this, the exporter writes
|
||||
# `foo.png`, `foo.001.png`, ... for one texture.
|
||||
def dedupe_images():
|
||||
canonical = {}
|
||||
for img in list(bpy.data.images):
|
||||
if not img.filepath:
|
||||
continue
|
||||
try:
|
||||
key = os.path.normpath(bpy.path.abspath(img.filepath))
|
||||
except Exception:
|
||||
continue
|
||||
if not key:
|
||||
continue
|
||||
if key not in canonical:
|
||||
canonical[key] = img
|
||||
continue
|
||||
keep = canonical[key]
|
||||
for mat in bpy.data.materials:
|
||||
if not mat.node_tree:
|
||||
continue
|
||||
for node in mat.node_tree.nodes:
|
||||
if node.type == 'TEX_IMAGE' and node.image is img:
|
||||
node.image = keep
|
||||
try:
|
||||
bpy.data.images.remove(img)
|
||||
except Exception:
|
||||
pass
|
||||
dedupe_images()
|
||||
try:
|
||||
bpy.ops.outliner.orphans_purge(do_local_ids=True,
|
||||
do_linked_ids=True,
|
||||
do_recursive=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for name in material_obj_names:
|
||||
obj = bpy.data.objects.get(name)
|
||||
|
||||
if obj and obj.type == 'MESH':
|
||||
@@ -499,6 +562,7 @@ for mapping in[CommandLineMapping()]:
|
||||
lod_obj_names = []
|
||||
if mapping_lods_path and os.path.exists(mapping_lods_path):
|
||||
print("Appending character LOD objects from: " + mapping_lods_path)
|
||||
before_lod_objects = set(bpy.data.objects)
|
||||
with bpy.data.libraries.load(mapping_lods_path) as (data_from, data_to):
|
||||
data_to.objects = [
|
||||
n for n in data_from.objects
|
||||
@@ -525,6 +589,16 @@ for mapping in[CommandLineMapping()]:
|
||||
if mod.type == 'ARMATURE':
|
||||
mod.object = body_arm
|
||||
print(f" LOD object '{obj.name}' -> mesh '{obj.data.name}'")
|
||||
# The lods blend also contains its own body armature. Drop appended
|
||||
# armature objects so their data blocks cannot steal the stem
|
||||
# skeleton name during export.
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj in before_lod_objects or obj.type != 'ARMATURE':
|
||||
continue
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
for ad in list(bpy.data.armatures):
|
||||
if ad.users == 0:
|
||||
bpy.data.armatures.remove(ad)
|
||||
# -------------------------------------------------------------
|
||||
|
||||
# Triangulate body part meshes before OGRE export to prevent the exporter's
|
||||
@@ -560,16 +634,53 @@ for mapping in[CommandLineMapping()]:
|
||||
|
||||
armobj = bpy.data.objects.get(mapping.armature_name)
|
||||
armobj.data.name = armobj.name
|
||||
print("EXPORT ARMATURE DATA: obj=%s data=%s all=%s" % (
|
||||
armobj.name, armobj.data.name,
|
||||
[(a.name, a.users) for a in bpy.data.armatures]))
|
||||
|
||||
# ---- Fix: sync Image filepath to Image name ----
|
||||
# Blender silently reuses existing Images when appending
|
||||
# from libraries, so the Image's filepath may still point
|
||||
# to the old texture even after the user renamed it.
|
||||
# blender2ogre writes the filepath to .material files,
|
||||
# so we must update filepath to match the Image name.
|
||||
# ---- Prefix image resources per stem ----
|
||||
# All age directories share the "Characters" resource group, so texture
|
||||
# file names must be unique across ages/sexes. Adult gets
|
||||
# `male-...`/`female-...`; new ages get `baby-male-...` etc. The prefix
|
||||
# is idempotent (an image already prefixed with the stem is left alone).
|
||||
import re as _re
|
||||
seen_imgs = set()
|
||||
for name in obj_names:
|
||||
stem = mapping.armature_name
|
||||
|
||||
def _unique_image_name(base):
|
||||
if bpy.data.images.get(base) is None:
|
||||
return base
|
||||
i = 1
|
||||
while bpy.data.images.get(f"{base}.{i:03d}") is not None:
|
||||
i += 1
|
||||
return f"{base}.{i:03d}"
|
||||
|
||||
image_obj_names = list(obj_names)
|
||||
image_obj_names += [o.name for o in hair_own_skel_objs]
|
||||
# LOD impostors use the already-unique `charlod_<stem>-atlas.png` /
|
||||
# `charlod_<stem>` material created by generate_lods.py; prefixing them
|
||||
# again would make the material reference a different file/format.
|
||||
# Normalize their image format to the filename extension, though; some
|
||||
# Blender versions keep the image blob as TARGA even when saved as PNG.
|
||||
for name in lod_obj_names:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if not obj or obj.type != 'MESH':
|
||||
continue
|
||||
for slot in obj.material_slots:
|
||||
if not slot.material or not slot.material.node_tree:
|
||||
continue
|
||||
for node in slot.material.node_tree.nodes:
|
||||
if node.type == 'TEX_IMAGE' and node.image:
|
||||
fp = node.image.filepath.lower()
|
||||
if fp.endswith(".png"):
|
||||
node.image.file_format = 'PNG'
|
||||
elif fp.endswith(".jpg") or fp.endswith(".jpeg"):
|
||||
node.image.file_format = 'JPEG'
|
||||
elif fp.endswith(".tga"):
|
||||
node.image.file_format = 'TARGA'
|
||||
output_dir = os.path.dirname(mapping.gltf_path)
|
||||
|
||||
for name in image_obj_names:
|
||||
obj = bpy.data.objects.get(name)
|
||||
if obj and obj.type == 'MESH':
|
||||
for slot in obj.material_slots:
|
||||
@@ -577,28 +688,50 @@ for mapping in[CommandLineMapping()]:
|
||||
for node in slot.material.node_tree.nodes:
|
||||
if node.type == 'TEX_IMAGE' and node.image:
|
||||
img = node.image
|
||||
if img.name not in seen_imgs:
|
||||
seen_imgs.add(img.name)
|
||||
# Strip Blender auto-suffix (.NNN)
|
||||
clean = _re.sub(
|
||||
r'\.\d{3}$', '', img.name)
|
||||
# Extract just the filename from
|
||||
# the current filepath
|
||||
old_name = os.path.basename(
|
||||
img.filepath)
|
||||
if old_name != clean and clean:
|
||||
# Rebuild filepath with new
|
||||
# filename
|
||||
dirpart = os.path.dirname(
|
||||
img.filepath)
|
||||
new_path = os.path.join(
|
||||
dirpart, clean)
|
||||
print(f" Updating Image "
|
||||
f"'{img.name}': "
|
||||
f"filepath "
|
||||
f"'{old_name}' -> "
|
||||
f"'{clean}'")
|
||||
img.filepath = new_path
|
||||
if img.name in seen_imgs:
|
||||
continue
|
||||
seen_imgs.add(img.name)
|
||||
clean = _re.sub(r'\.\d{3}$', '', img.name)
|
||||
old_name = os.path.basename(img.filepath)
|
||||
if not old_name:
|
||||
# Packed/generated image: use the datablock
|
||||
# name as the file base.
|
||||
old_name = clean
|
||||
if not os.path.splitext(old_name)[1]:
|
||||
old_name += ".png"
|
||||
# Strip Blender auto-suffix before the extension.
|
||||
base, ext = os.path.splitext(old_name)
|
||||
base = _re.sub(r'\.\d{3}$', '', base)
|
||||
if not ext:
|
||||
ext = ".png"
|
||||
if (base.startswith(stem + "-") or
|
||||
base.startswith(stem + "_")):
|
||||
new_base = base
|
||||
else:
|
||||
new_base = f"{stem}-{base}"
|
||||
image_name = _unique_image_name(new_base)
|
||||
new_file = image_name + ext
|
||||
new_path = os.path.join(output_dir, new_file)
|
||||
if img.name != image_name:
|
||||
print(f" Prefixing Image "
|
||||
f"'{img.name}' -> '{image_name}' "
|
||||
f"({new_file})")
|
||||
img.name = image_name
|
||||
if new_file.lower().endswith(".png"):
|
||||
img.file_format = 'PNG'
|
||||
elif new_file.lower().endswith(".jpg") or \
|
||||
new_file.lower().endswith(".jpeg"):
|
||||
img.file_format = 'JPEG'
|
||||
elif new_file.lower().endswith(".tga"):
|
||||
img.file_format = 'TARGA'
|
||||
img.filepath = new_path
|
||||
img.filepath_raw = new_path
|
||||
try:
|
||||
img.save()
|
||||
except Exception as exc:
|
||||
print(f" WARNING: could not save image "
|
||||
f"'{img.name}': {exc}")
|
||||
seen_imgs.add(img.name)
|
||||
# ---------------------------------------------------
|
||||
|
||||
bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')
|
||||
|
||||
@@ -13,7 +13,7 @@ foreach(VEHICLE_FILE ${VEHICLES_SRC})
|
||||
add_custom_command(
|
||||
OUTPUT ${VEHICLE_OUTPUT_FILE} ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/resources/vehicles
|
||||
COMMAND ${BLENDER} ${CMAKE_CURRENT_SOURCE_DIR}/${VEHICLE_FILE}
|
||||
COMMAND ${BLENDER_LOCKED} ${CMAKE_CURRENT_SOURCE_DIR}/${VEHICLE_FILE}
|
||||
-b -Y -P
|
||||
${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_vehicles.py
|
||||
-- ${VEHICLE_OUTPUT_FILE}
|
||||
@@ -40,7 +40,7 @@ foreach(VEHICLE_FILE ${VEHICLES_GEN_SRC})
|
||||
add_custom_command(
|
||||
OUTPUT ${VEHICLE_OUTPUT_FILE} ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene ${VEHICLE_JSON_FILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/resources/vehicles
|
||||
COMMAND ${BLENDER} ${CMAKE_CURRENT_SOURCE_DIR}/${VEHICLE_FILE}
|
||||
COMMAND ${BLENDER_LOCKED} ${CMAKE_CURRENT_SOURCE_DIR}/${VEHICLE_FILE}
|
||||
-b -Y -P
|
||||
${CMAKE_SOURCE_DIR}/assets/blender/scripts/gen_vehicle.py
|
||||
-- ${VEHICLE_OUTPUT_FILE} ${VEHICLE_JSON_FILE} ${DOWNFORCE_${FILE_NAME}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,9 @@ set(EDITSCENE_SOURCES
|
||||
systems/ProceduralMeshSystem.cpp
|
||||
systems/CellGridSystem.cpp
|
||||
systems/DoorSystem.cpp
|
||||
systems/GeneratedInteriorService.cpp
|
||||
systems/VehicleStateStore.cpp
|
||||
systems/InteriorNpcSystem.cpp
|
||||
systems/DoorBuilder.cpp
|
||||
systems/StandaloneDoorSystem.cpp
|
||||
components/StandaloneDoorModule.cpp
|
||||
@@ -238,13 +241,17 @@ set(EDITSCENE_SOURCES
|
||||
lua/LuaSaveLoadApi.cpp
|
||||
lua/LuaGlobalStateApi.cpp
|
||||
lua/LuaCharacterStatsApi.cpp
|
||||
lua/LuaEmergencyApi.cpp
|
||||
lua/LuaScheduleScoringApi.cpp
|
||||
lua/LuaRostersApi.cpp
|
||||
lua/LuaDoorApi.cpp
|
||||
lua/LuaHudApi.cpp
|
||||
lua/LuaFurnitureApi.cpp
|
||||
lua/LuaTerrainApi.cpp
|
||||
lua/LuaSceneSwitchApi.cpp
|
||||
systems/TerrainTests.cpp
|
||||
systems/StatEffectsApplier.cpp
|
||||
systems/ActivityEffectHook.cpp
|
||||
)
|
||||
|
||||
set(EDITSCENE_HEADERS
|
||||
@@ -867,6 +874,31 @@ add_test(NAME scheduleScoringTest
|
||||
COMMAND schedule_scoring_test
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: Phase 6.8.7 roster Lua binding (headless)
|
||||
# ---------------------------------------------------------------------------
|
||||
# The pure derivation (roadlib/TownRosters) + the ecs.rosters binding;
|
||||
# no OGRE, no flecs — the provider pattern is exercised with a fake area.
|
||||
add_executable(rosters_lua_test
|
||||
tests/rosters_lua_test.cpp
|
||||
roadlib/TownRosters.cpp
|
||||
lua/LuaRostersApi.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(rosters_lua_test
|
||||
lua
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_include_directories(rosters_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
add_test(NAME rostersLuaTest
|
||||
COMMAND rosters_lua_test
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: Phase 6.8.9 character stat container (headless)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -875,6 +907,7 @@ add_test(NAME scheduleScoringTest
|
||||
add_executable(character_stats_test
|
||||
tests/character_stats_test.cpp
|
||||
systems/CharacterStatStore.cpp
|
||||
systems/ActivityEffectHook.cpp
|
||||
lua/LuaCharacterStatsApi.cpp
|
||||
)
|
||||
|
||||
@@ -963,6 +996,8 @@ add_library(LayoutLib STATIC
|
||||
layoutlib/LayoutLang.hpp
|
||||
layoutlib/LayoutEval.cpp
|
||||
layoutlib/LayoutEval.hpp
|
||||
layoutlib/Stairwell.cpp
|
||||
layoutlib/Stairwell.hpp
|
||||
)
|
||||
|
||||
target_include_directories(LayoutLib PUBLIC
|
||||
@@ -988,8 +1023,14 @@ add_library(RoadGeometryLib STATIC
|
||||
roadlib/TownSchedule.hpp
|
||||
roadlib/TownScheduleScore.cpp
|
||||
roadlib/TownScheduleScore.hpp
|
||||
roadlib/TownRosters.cpp
|
||||
roadlib/TownRosters.hpp
|
||||
roadlib/TownRostersWeeks.cpp
|
||||
roadlib/TownRostersWeeks.hpp
|
||||
roadlib/PoiDatabase.cpp
|
||||
roadlib/PoiDatabase.hpp
|
||||
roadlib/InteriorScene.cpp
|
||||
roadlib/InteriorScene.hpp
|
||||
)
|
||||
|
||||
target_include_directories(RoadGeometryLib PUBLIC
|
||||
@@ -1033,6 +1074,9 @@ target_include_directories(RoadGeometryDemo PRIVATE
|
||||
# "Ax Ay Az Bx By Bz Cx Cy Cz" analyse a single custom configuration.
|
||||
add_executable(road_geometry_overlap_test
|
||||
tests/road_geometry_overlap_test.cpp
|
||||
systems/CharacterStatStore.cpp
|
||||
systems/StatEffectsApplier.cpp
|
||||
systems/ActivityEffectHook.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(road_geometry_overlap_test
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
#include "EditorApp.hpp"
|
||||
#include "GameMode.hpp"
|
||||
#include <OgreMaterialManager.h>
|
||||
@@ -73,6 +74,8 @@
|
||||
#include "lua/LuaGlobalStateApi.hpp"
|
||||
#include "lua/LuaCharacterStatsApi.hpp"
|
||||
#include "lua/LuaScheduleScoringApi.hpp"
|
||||
#include "lua/LuaRostersApi.hpp"
|
||||
#include "lua/LuaEmergencyApi.hpp"
|
||||
#include "systems/CharacterStatStore.hpp"
|
||||
#include "lua/LuaSceneSwitchApi.hpp"
|
||||
#include "systems/PlayerControllerSystem.hpp"
|
||||
@@ -82,6 +85,7 @@
|
||||
#include "systems/SaveLoadDialog.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
#include "components/EntityName.hpp"
|
||||
#include "components/InteriorNpc.hpp"
|
||||
#include "components/Transform.hpp"
|
||||
#include "components/Renderable.hpp"
|
||||
#include "components/EditorMarker.hpp"
|
||||
@@ -132,6 +136,9 @@
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "systems/ActuatorSystem.hpp"
|
||||
#include "systems/DoorSystem.hpp"
|
||||
#include "systems/GeneratedInteriorService.hpp"
|
||||
#include "systems/VehicleStateStore.hpp"
|
||||
#include "systems/InteriorNpcSystem.hpp"
|
||||
#include "systems/StandaloneDoorSystem.hpp"
|
||||
#include "systems/EventHandlerSystem.hpp"
|
||||
#include "systems/EventBus.hpp"
|
||||
@@ -863,6 +870,14 @@ void EditorApp::setup()
|
||||
m_doorSystem = std::make_unique<DoorSystem>(m_world);
|
||||
m_doorSystem->setEditorApp(this);
|
||||
|
||||
/* Phase 8.2: in-memory generated interior documents + the
|
||||
* "generated:<gridUid>" scene-switch scheme. */
|
||||
m_generatedInteriorService =
|
||||
std::make_unique<GeneratedInteriorService>(m_world,
|
||||
m_sceneMgr);
|
||||
m_interiorNpcSystem =
|
||||
std::make_unique<InteriorNpcSystem>(m_world, this);
|
||||
|
||||
// Standalone doors (F2, same DoorBuilder as CellGrid doors)
|
||||
m_standaloneDoorSystem =
|
||||
std::make_unique<StandaloneDoorSystem>(m_world, m_sceneMgr);
|
||||
@@ -907,9 +922,20 @@ void EditorApp::setup()
|
||||
if (m_gameMode == GameMode::Game) {
|
||||
GlobalStateStore::getInstance().loadFromFile(
|
||||
"global_state.json");
|
||||
/* 6.8.9: explicit stat values are a cross-session
|
||||
* cache too. Definitions re-declare later from
|
||||
* scripts; tests can disable this load for fixture
|
||||
* isolation. */
|
||||
if (CharacterStatStore::getInstance().autoLoadEnabled())
|
||||
CharacterStatStore::getInstance().loadFromFile(
|
||||
"character_stats.json");
|
||||
} else {
|
||||
GlobalStateStore::getInstance().setAutoSaveEnabled(false);
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
CharacterStatStore::getInstance().setAutoSaveEnabled(
|
||||
false);
|
||||
CharacterStatStore::getInstance().deserialize(
|
||||
nlohmann::json::object());
|
||||
}
|
||||
|
||||
m_characterClassSystem =
|
||||
@@ -1027,11 +1053,34 @@ void EditorApp::setup()
|
||||
editScene::registerLuaGlobalStateApi(L);
|
||||
editScene::registerLuaCharacterStatsApi(L);
|
||||
editScene::registerLuaScheduleScoringApi(L);
|
||||
editScene::registerLuaRostersApi(L);
|
||||
/* 6.8.14: the emergency layer (threshold registration
|
||||
* + begin/end/active bindings). */
|
||||
editScene::registerLuaEmergencyApi(L);
|
||||
editScene::registerLuaDoorApi(L);
|
||||
editScene::registerLuaHudApi(L);
|
||||
editScene::registerLuaFurnitureApi(L, &m_world);
|
||||
editScene::setLuaCellGridSystem(m_cellGridSystem.get());
|
||||
editScene::setLuaHudSystem(m_gameHudSystem.get());
|
||||
/* 6.8.7: the Lua roster binding reads the same
|
||||
* per-area store the C++ accessor surfaces. */
|
||||
editScene::setLuaRostersProvider(
|
||||
[](int areaIndex, void *user,
|
||||
TownRosters::Rosters &out) {
|
||||
auto *pop = static_cast<
|
||||
NpcPopulationSystem *>(
|
||||
user);
|
||||
if (!pop)
|
||||
return false;
|
||||
const TownRosters::Rosters
|
||||
*rosters =
|
||||
pop->areaRosters(
|
||||
areaIndex);
|
||||
if (!rosters)
|
||||
return false;
|
||||
out = *rosters;
|
||||
return true;
|
||||
},
|
||||
m_npcPopulationSystem.get()); editScene::setLuaHudSystem(m_gameHudSystem.get());
|
||||
editScene::setSceneSwitchEditorApp(this);
|
||||
editScene::setLuaPlayerCharacterResolver(
|
||||
[this]() { return getPlayerCharacterEntity(); });
|
||||
@@ -1315,21 +1364,32 @@ void EditorApp::clearScene()
|
||||
|
||||
void EditorApp::startNewGame(const Ogre::String &scenePath)
|
||||
{
|
||||
/* The caller may pass storage owned by an entity that clearScene()
|
||||
* destroys — the startup menu calls this with
|
||||
* StartupMenuComponent::newGameScene, and the menu entity dies in
|
||||
* the teardown. Keep a copy for the whole load. */
|
||||
const Ogre::String path = scenePath;
|
||||
VehicleStateStore::getInstance().clear();
|
||||
if (m_interiorNpcSystem)
|
||||
m_interiorNpcSystem->clear();
|
||||
if (m_npcTrafficSystem)
|
||||
m_npcTrafficSystem->deserializeStolenCars(
|
||||
nlohmann::json::array());
|
||||
clearScene();
|
||||
/* F9: a new game starts from system defaults, not from the
|
||||
* cross-session cache. */
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
if (serializer.loadFromFile(scenePath, m_uiSystem.get())) {
|
||||
if (serializer.loadFromFile(path, m_uiSystem.get())) {
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
prefabSys.resolveInstances();
|
||||
SceneScriptSystem::loadPendingScripts(m_world);
|
||||
SceneScriptSystem::sendSceneLoaded(scenePath);
|
||||
SceneScriptSystem::sendSceneLoaded(path);
|
||||
setGamePlayState(GamePlayState::Playing);
|
||||
m_currentBaseScene = scenePath;
|
||||
m_currentBaseScene = path;
|
||||
m_playTime = 0.0f;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game started: loaded scene " + scenePath);
|
||||
"Game started: loaded scene " + path);
|
||||
|
||||
setupPlayerCharacter();
|
||||
|
||||
@@ -1424,7 +1484,20 @@ bool EditorApp::switchScene(const Ogre::String &scenePath,
|
||||
"EditorApp::switchScene: empty scene path");
|
||||
return false;
|
||||
}
|
||||
if (!std::filesystem::exists(scenePath)) {
|
||||
SceneSwitchOptions effective = opts;
|
||||
if (GeneratedInteriorService::isScheme(scenePath) &&
|
||||
effective.sceneDocument.is_null()) {
|
||||
/* Phase 8.2: resolve the generated-interior scheme to an
|
||||
* in-memory scene document (the path stays the display
|
||||
* name). The current base scene is the interior's return
|
||||
* target. */
|
||||
if (!m_generatedInteriorService ||
|
||||
!m_generatedInteriorService->resolveGeneratedInterior(
|
||||
scenePath, m_currentBaseScene,
|
||||
effective.sceneDocument))
|
||||
return false;
|
||||
} else if (opts.sceneDocument.is_null() &&
|
||||
!std::filesystem::exists(scenePath)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp::switchScene: file not found: " + scenePath);
|
||||
return false;
|
||||
@@ -1435,7 +1508,7 @@ bool EditorApp::switchScene(const Ogre::String &scenePath,
|
||||
* mutate the world mid-frame. A new request replaces a queued
|
||||
* one. */
|
||||
m_pendingSceneSwitchPath = scenePath;
|
||||
m_pendingSceneSwitchOptions = opts;
|
||||
m_pendingSceneSwitchOptions = effective;
|
||||
m_sceneSwitchPending = true;
|
||||
|
||||
/* Raise the loading cover NOW (game mode): the switch itself runs
|
||||
@@ -1495,6 +1568,98 @@ void EditorApp::processPendingSceneSwitch()
|
||||
(switchTraceClock() - loadStartMs) * 1000.0);
|
||||
}
|
||||
|
||||
/* Phase 8.2: deferred load-game re-entry. loadGame loads the exterior
|
||||
* base scene first, then waits here until the streamed town has rebuilt
|
||||
* the saved building and registered its plan; the interior document is
|
||||
* resolved and the scene switch runs with the saved snapshot restored
|
||||
* on the interior (applyLoadedSaveState, at the end of
|
||||
* performSceneSwitch). */
|
||||
void EditorApp::processPendingInteriorReentry()
|
||||
{
|
||||
if (!m_pendingInteriorReentry)
|
||||
return;
|
||||
if (m_gameMode != GameMode::Game) {
|
||||
/* Left the game session: the pending load is moot. */
|
||||
m_pendingInteriorReentry = false;
|
||||
m_pendingSavedGame = nlohmann::json();
|
||||
return;
|
||||
}
|
||||
|
||||
auto restoreInExterior = [&](const std::string &why) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game load: " + why + "; restoring in the exterior "
|
||||
"scene");
|
||||
m_pendingInteriorReentry = false;
|
||||
nlohmann::json saved = std::move(m_pendingSavedGame);
|
||||
m_pendingSavedGame = nlohmann::json();
|
||||
applyLoadedSaveState(saved, m_pendingSaveSlot);
|
||||
};
|
||||
|
||||
if (!m_pendingSavedGame.contains("saveGame")) {
|
||||
m_pendingInteriorReentry = false;
|
||||
m_pendingSavedGame = nlohmann::json();
|
||||
return;
|
||||
}
|
||||
const nlohmann::json &saveGame = m_pendingSavedGame["saveGame"];
|
||||
if (!saveGame.contains("interior") ||
|
||||
!saveGame["interior"].is_object() ||
|
||||
!m_generatedInteriorService) {
|
||||
restoreInExterior("no usable interior marker");
|
||||
return;
|
||||
}
|
||||
const nlohmann::json marker = saveGame["interior"];
|
||||
const std::string scheme = marker.value("scene", std::string());
|
||||
const std::string sourceScene =
|
||||
marker.value("sourceScene", m_currentBaseScene);
|
||||
if (scheme.empty()) {
|
||||
restoreInExterior("interior marker has no scene");
|
||||
return;
|
||||
}
|
||||
|
||||
/* The streamed town needs a few frames to rebuild the building
|
||||
* and register its plan; probe every 30 frames (~0.5 s) so the
|
||||
* failure logs stay readable. */
|
||||
++m_pendingInteriorReentryFrames;
|
||||
if (m_pendingInteriorReentryFrames > 1800) {
|
||||
restoreInExterior("generated interior '" + scheme +
|
||||
"' unavailable");
|
||||
return;
|
||||
}
|
||||
if (m_pendingInteriorReentryFrames > 1 &&
|
||||
m_pendingInteriorReentryFrames % 30 != 0)
|
||||
return;
|
||||
nlohmann::json document;
|
||||
if (!m_generatedInteriorService->resolveGeneratedInterior(
|
||||
scheme, sourceScene, document))
|
||||
return;
|
||||
|
||||
m_pendingInteriorReentry = false;
|
||||
m_applySaveStateAfterSwitch = std::move(m_pendingSavedGame);
|
||||
m_pendingSavedGame = nlohmann::json();
|
||||
|
||||
SceneSwitchOptions opts;
|
||||
opts.sceneDocument = document;
|
||||
if (marker.contains("posX") && marker.contains("posZ")) {
|
||||
opts.hasPosition = true;
|
||||
opts.posX = marker.value("posX", 0.0);
|
||||
opts.posY = marker.value("posY", 0.0);
|
||||
opts.posZ = marker.value("posZ", 0.0);
|
||||
if (marker.contains("yaw")) {
|
||||
opts.hasRotation = true;
|
||||
opts.rotation = Ogre::Quaternion(
|
||||
Ogre::Degree(marker.value("yaw", 0.0f)),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
}
|
||||
if (!switchScene(scheme, opts)) {
|
||||
/* Resolution raced a scene change: restore the snapshot in
|
||||
* the exterior rather than losing it. */
|
||||
nlohmann::json saved = std::move(m_applySaveStateAfterSwitch);
|
||||
m_applySaveStateAfterSwitch = nlohmann::json();
|
||||
applyLoadedSaveState(saved, m_pendingSaveSlot);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::renderSceneSwitchCover()
|
||||
{
|
||||
if (m_gameMode != GameMode::Game || m_sceneSwitchCoverFrames <= 0)
|
||||
@@ -1587,6 +1752,47 @@ static void scanSceneEntityJson(const nlohmann::json &entityJson,
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::populateInteriorNpcs()
|
||||
{
|
||||
if (!m_interiorNpcSystem)
|
||||
return;
|
||||
flecs::entity interiorGrid;
|
||||
m_world.each([&](flecs::entity e, CellGridComponent &g) {
|
||||
if (interiorGrid.is_alive())
|
||||
return;
|
||||
if (g.generationMode == "interiorOnly" &&
|
||||
g.gridUid.rfind("town-", 0) == 0)
|
||||
interiorGrid = e;
|
||||
});
|
||||
m_interiorNpcSystem->populate(interiorGrid);
|
||||
m_interiorNpcRetryFrames =
|
||||
m_interiorNpcSystem->needsRetry() ? 120 : 0;
|
||||
}
|
||||
|
||||
void EditorApp::captureVehicleStates()
|
||||
{
|
||||
if (m_gameMode != GameMode::Game)
|
||||
return;
|
||||
VehicleStateStore::getInstance().capture(
|
||||
m_world, m_sceneMgr, m_currentBaseScene,
|
||||
m_renderOriginSystem.get());
|
||||
}
|
||||
|
||||
void EditorApp::applyVehicleStates(const Ogre::String &scenePath)
|
||||
{
|
||||
if (m_gameMode != GameMode::Game || scenePath.empty())
|
||||
return;
|
||||
const int applied = VehicleStateStore::getInstance().apply(
|
||||
m_world, m_sceneMgr, scenePath, m_renderOriginSystem.get(),
|
||||
m_physicsSystem ? m_physicsSystem->getPhysicsWrapper() :
|
||||
nullptr);
|
||||
if (applied > 0)
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: restored " +
|
||||
std::to_string(applied) +
|
||||
" player-touched vehicle(s) for " + scenePath);
|
||||
}
|
||||
|
||||
bool EditorApp::performSceneSwitch(const Ogre::String &scenePath,
|
||||
const SceneSwitchOptions &opts)
|
||||
{
|
||||
@@ -1601,15 +1807,27 @@ bool EditorApp::performSceneSwitch(const Ogre::String &scenePath,
|
||||
m_renderOriginSystem->rebase(JPH::DVec3(0, 0, 0));
|
||||
if (m_camera)
|
||||
m_camera->resetPose();
|
||||
if (m_uiSystem) {
|
||||
if (!opts.sceneDocument.is_null()) {
|
||||
/* Phase 8.2: in-memory generated interior. */
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
serializer.loadFromJson(opts.sceneDocument);
|
||||
m_activeGeneratedInterior = scenePath;
|
||||
m_activeGeneratedSourceScene =
|
||||
m_currentBaseScene;
|
||||
} else if (m_uiSystem) {
|
||||
/* loadScene wires the serializer, navigation
|
||||
* bookmarks and scene script events. */
|
||||
m_uiSystem->loadScene(scenePath);
|
||||
m_activeGeneratedInterior.clear();
|
||||
m_activeGeneratedSourceScene.clear();
|
||||
m_currentBaseScene = scenePath;
|
||||
} else {
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
serializer.loadFromFile(scenePath, nullptr);
|
||||
m_activeGeneratedInterior.clear();
|
||||
m_activeGeneratedSourceScene.clear();
|
||||
m_currentBaseScene = scenePath;
|
||||
}
|
||||
m_currentBaseScene = scenePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1620,8 +1838,13 @@ bool EditorApp::performSceneSwitch(const Ogre::String &scenePath,
|
||||
bool newHasCamera = false;
|
||||
bool newHasController = false;
|
||||
try {
|
||||
std::ifstream in(scenePath);
|
||||
nlohmann::json scene = nlohmann::json::parse(in);
|
||||
nlohmann::json scene;
|
||||
if (!opts.sceneDocument.is_null()) {
|
||||
scene = opts.sceneDocument;
|
||||
} else {
|
||||
std::ifstream in(scenePath);
|
||||
scene = nlohmann::json::parse(in);
|
||||
}
|
||||
if (scene.contains("entities") && scene["entities"].is_array())
|
||||
for (const auto &ent : scene["entities"])
|
||||
scanSceneEntityJson(ent, newHasCamera,
|
||||
@@ -1645,6 +1868,23 @@ bool EditorApp::performSceneSwitch(const Ogre::String &scenePath,
|
||||
|
||||
flecs::entity carriedCharacter = flecs::entity::null();
|
||||
const bool preservePlayer = !newHasController && controller.is_alive();
|
||||
/* A controller carried into the current scene by an EARLIER switch
|
||||
* was stripped of its markers (so that switch could keep it), and
|
||||
* destroySceneEntities() below therefore cannot see it. When the
|
||||
* target scene brings its own controller, drop the leftover so
|
||||
* the fresh one wins the "player" lookup instead of resolving a
|
||||
* dead target. */
|
||||
if (newHasController && controller.is_alive() &&
|
||||
!controller.has<EditorMarkerComponent>() &&
|
||||
!controller.has<RuntimeMarkerComponent>()) {
|
||||
flecs::entity leftoverCharacter = getPlayerCharacterEntity();
|
||||
if (leftoverCharacter.is_alive() &&
|
||||
!leftoverCharacter.has<EditorMarkerComponent>() &&
|
||||
!leftoverCharacter.has<RuntimeMarkerComponent>())
|
||||
leftoverCharacter.destruct();
|
||||
controller.destruct();
|
||||
controller = flecs::entity::null();
|
||||
}
|
||||
if (preservePlayer) {
|
||||
carriedCharacter = getPlayerCharacterEntity();
|
||||
|
||||
@@ -1687,25 +1927,85 @@ bool EditorApp::performSceneSwitch(const Ogre::String &scenePath,
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 8.2 follow-up: snapshot player-touched vehicles before the
|
||||
* teardown so a later load of this scene restores their poses
|
||||
* (authored cars and carjacked runtime cars alike). While leaving
|
||||
* a generated interior there is nothing to capture: its exterior
|
||||
* snapshot was taken on entry. */
|
||||
if (m_activeGeneratedInterior.empty())
|
||||
captureVehicleStates();
|
||||
|
||||
destroySceneEntities();
|
||||
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
if (!serializer.loadFromFile(scenePath, m_uiSystem.get())) {
|
||||
bool loaded = false;
|
||||
if (!opts.sceneDocument.is_null())
|
||||
loaded = serializer.loadFromJson(opts.sceneDocument,
|
||||
m_uiSystem.get());
|
||||
else
|
||||
loaded = serializer.loadFromFile(scenePath,
|
||||
m_uiSystem.get());
|
||||
if (!loaded) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: failed to load scene: " +
|
||||
serializer.getLastError());
|
||||
if (!m_applySaveStateAfterSwitch.is_null()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"EditorApp: discarding the pending load-game "
|
||||
"state after the failed interior load");
|
||||
m_applySaveStateAfterSwitch = nlohmann::json();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
prefabSys.resolveInstances();
|
||||
/* Phase 8.2 follow-up: a file scene (the exterior) restores the
|
||||
* player-touched cars; an in-memory interior has none. */
|
||||
if (opts.sceneDocument.is_null())
|
||||
applyVehicleStates(scenePath);
|
||||
SceneScriptSystem::loadPendingScripts(m_world);
|
||||
SceneScriptSystem::sendSceneLoaded(scenePath);
|
||||
m_currentBaseScene = scenePath;
|
||||
/* Phase 8.2: an in-memory document is a generated interior — the
|
||||
* BASE scene stays the exterior one (saves stay anchored there);
|
||||
* file switches leave any generated interior and become the base. */
|
||||
if (!opts.sceneDocument.is_null()) {
|
||||
m_activeGeneratedInterior = scenePath;
|
||||
m_activeGeneratedSourceScene = m_currentBaseScene;
|
||||
/* Interiors get no sky ambient; the document's declared
|
||||
* ambient replaces the scene one for the stay and is
|
||||
* restored on the way out. */
|
||||
const float amb = opts.sceneDocument
|
||||
.value("interior", nlohmann::json())
|
||||
.value("ambient", 0.5f);
|
||||
if (!m_interiorSavedAmbientValid) {
|
||||
m_interiorSavedAmbient =
|
||||
m_sceneMgr->getAmbientLight();
|
||||
m_interiorSavedAmbientValid = true;
|
||||
}
|
||||
m_sceneMgr->setAmbientLight(
|
||||
Ogre::ColourValue(amb, amb, amb));
|
||||
} else {
|
||||
m_activeGeneratedInterior.clear();
|
||||
m_activeGeneratedSourceScene.clear();
|
||||
m_currentBaseScene = scenePath;
|
||||
if (m_interiorSavedAmbientValid) {
|
||||
m_sceneMgr->setAmbientLight(m_interiorSavedAmbient);
|
||||
m_interiorSavedAmbientValid = false;
|
||||
}
|
||||
}
|
||||
/* m_playTime keeps accumulating across the switch (same session). */
|
||||
setGamePlayState(GamePlayState::Playing);
|
||||
|
||||
setupPlayerCharacter();
|
||||
|
||||
/* Phase 8.2 follow-up: the exterior detail tier skips indoor NPCs
|
||||
* (home/work/leisure); a generated interior populates them here
|
||||
* from the same schedule state. A file scene has no interior. */
|
||||
if (!opts.sceneDocument.is_null())
|
||||
populateInteriorNpcs();
|
||||
else if (m_interiorNpcSystem)
|
||||
m_interiorNpcSystem->clear();
|
||||
|
||||
/* Camera handoff: a scene camera takes over the viewport only
|
||||
* when no player controller is active after the switch (the
|
||||
* PlayerControllerSystem drives the editor camera otherwise).
|
||||
@@ -1817,6 +2117,21 @@ bool EditorApp::performSceneSwitch(const Ogre::String &scenePath,
|
||||
* to build. */
|
||||
m_sceneSwitchCoverFrames = SCENE_SWITCH_COVER_FRAMES;
|
||||
|
||||
/* Phase 8.2: a deferred load-game re-entry restores the saved
|
||||
* snapshot on the freshly loaded interior (registry spawns,
|
||||
* runtime entities, controller targeting). */
|
||||
if (!m_applySaveStateAfterSwitch.is_null()) {
|
||||
nlohmann::json saved =
|
||||
std::move(m_applySaveStateAfterSwitch);
|
||||
m_applySaveStateAfterSwitch = nlohmann::json();
|
||||
applyLoadedSaveState(saved, m_pendingSaveSlot);
|
||||
/* applyLoadedSaveState destroys every spawned character and
|
||||
* respawns the save's persistent set; the interior's
|
||||
* scheduled NPCs must be materialized again. */
|
||||
if (!m_activeGeneratedInterior.empty())
|
||||
populateInteriorNpcs();
|
||||
}
|
||||
|
||||
EventBus::getInstance().send("scene_ready");
|
||||
return true;
|
||||
}
|
||||
@@ -1840,6 +2155,8 @@ void EditorApp::saveGame(const std::string &slotPath,
|
||||
auto ®istry = CharacterRegistry::getSingleton();
|
||||
m_world.query<CharacterIdentityComponent>().each(
|
||||
[&](flecs::entity e, CharacterIdentityComponent &ci) {
|
||||
if (e.has<InteriorNpcTag>())
|
||||
return; /* regenerated from the schedule */
|
||||
auto *rec = registry.findCharacter(ci.registryId);
|
||||
if (!rec || !e.has<TransformComponent>())
|
||||
return;
|
||||
@@ -1864,6 +2181,43 @@ void EditorApp::saveGame(const std::string &slotPath,
|
||||
}
|
||||
saveData["saveGame"]["playerCharacterId"] = playerCharId;
|
||||
|
||||
/* Phase 8.2: saving while inside a generated interior only records
|
||||
* a marker — the exterior base scene above and all normal state
|
||||
* stay authoritative; loading rebuilds the interior from the plan. */
|
||||
if (!m_activeGeneratedInterior.empty() && playerChar.is_alive() &&
|
||||
playerChar.has<TransformComponent>()) {
|
||||
const auto &pt = playerChar.get<TransformComponent>();
|
||||
const Ogre::Vector3 pos =
|
||||
pt.node ? pt.node->_getDerivedPosition() : pt.position;
|
||||
const Ogre::Quaternion rot =
|
||||
pt.node ? pt.node->_getDerivedOrientation() : pt.rotation;
|
||||
JPH::DVec3 wp(pos.x, pos.y, pos.z);
|
||||
if (m_renderOriginSystem)
|
||||
wp = m_renderOriginSystem->renderToWorld(pos);
|
||||
const Ogre::Vector3 fwd = rot * Ogre::Vector3::UNIT_Z;
|
||||
const float yaw =
|
||||
Ogre::Math::ATan2(fwd.x, fwd.z).valueDegrees();
|
||||
saveData["saveGame"]["interior"] = {
|
||||
{ "scene", m_activeGeneratedInterior },
|
||||
{ "sourceScene", m_activeGeneratedSourceScene },
|
||||
{ "posX", wp.GetX() },
|
||||
{ "posY", wp.GetY() },
|
||||
{ "posZ", wp.GetZ() },
|
||||
{ "yaw", yaw }
|
||||
};
|
||||
}
|
||||
|
||||
/* Phase 8.2 follow-up: player-touched vehicles keep their last
|
||||
* pose across scene switches and saves. While inside a generated
|
||||
* interior the exterior snapshot is already in the store (captured
|
||||
* on entry); do not overwrite it from the interior world. */
|
||||
if (m_activeGeneratedInterior.empty())
|
||||
captureVehicleStates();
|
||||
saveData["vehicleState"] = VehicleStateStore::getInstance().serialize();
|
||||
if (m_npcTrafficSystem)
|
||||
saveData["stolenVehicles"] =
|
||||
m_npcTrafficSystem->serializeStolenCars();
|
||||
|
||||
saveData["characterRegistry"] = registry.serialize();
|
||||
saveData["containerState"] =
|
||||
ContainerStateRegistry::getInstance().serialize();
|
||||
@@ -1871,12 +2225,20 @@ void EditorApp::saveGame(const std::string &slotPath,
|
||||
saveData["globalState"] = GlobalStateStore::getInstance().serialize();
|
||||
saveData["characterStats"] =
|
||||
CharacterStatStore::getInstance().serialize();
|
||||
/* 6.8.10: the weekly schedule store (weeks, bindings, usage,
|
||||
* the stats snapshot used, revision markers) is scene-
|
||||
* independent state and rides the save game. */
|
||||
if (m_npcPopulationSystem)
|
||||
saveData["npcWeekSchedules"] =
|
||||
m_npcPopulationSystem->serializeWeekSchedules();
|
||||
|
||||
/* Runtime entities — skip characters and player controller */
|
||||
saveData["runtimeEntities"] = nlohmann::json::array();
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
m_world.query<RuntimeMarkerComponent>().each(
|
||||
[&](flecs::entity e, const RuntimeMarkerComponent &) {
|
||||
if (e.has<InteriorNpcTag>())
|
||||
return;
|
||||
if (e.has<CharacterIdentityComponent>())
|
||||
return;
|
||||
if (e.has<PlayerControllerComponent>())
|
||||
@@ -1896,6 +2258,8 @@ void EditorApp::saveGame(const std::string &slotPath,
|
||||
};
|
||||
m_world.query<CharacterIdentityComponent>().each(
|
||||
[&](flecs::entity e, CharacterIdentityComponent &ci) {
|
||||
if (e.has<InteriorNpcTag>())
|
||||
return;
|
||||
nlohmann::json entityJson =
|
||||
serializer.serializeEntity(e);
|
||||
nlohmann::json filtered;
|
||||
@@ -1920,37 +2284,10 @@ void EditorApp::saveGame(const std::string &slotPath,
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::loadGame(const std::string &slotPath)
|
||||
void EditorApp::applyLoadedSaveState(const nlohmann::json &saveData,
|
||||
const std::string &slotPath)
|
||||
{
|
||||
EventBus::getInstance().send("load_game_requested");
|
||||
|
||||
nlohmann::json saveData;
|
||||
if (!SaveLoadSystem::readSaveFile(slotPath, saveData)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Failed to read save file: " + slotPath);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string baseScene = saveData["saveGame"].value("baseScene", "");
|
||||
if (baseScene.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Save file missing base scene reference");
|
||||
return;
|
||||
}
|
||||
|
||||
clearScene();
|
||||
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
if (!serializer.loadFromFile(baseScene, m_uiSystem.get())) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Failed to load base scene: " +
|
||||
serializer.getLastError());
|
||||
return;
|
||||
}
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
prefabSys.resolveInstances();
|
||||
SceneScriptSystem::loadPendingScripts(m_world);
|
||||
SceneScriptSystem::sendSceneLoaded(baseScene);
|
||||
|
||||
/* Restore registries */
|
||||
auto ®istry = CharacterRegistry::getSingleton();
|
||||
@@ -1977,6 +2314,13 @@ void EditorApp::loadGame(const std::string &slotPath)
|
||||
else
|
||||
CharacterStatStore::getInstance().deserialize(
|
||||
nlohmann::json::object());
|
||||
/* 6.8.10: restore the weekly schedule store (weeks, bindings,
|
||||
* usage, the stats snapshot used and the revision markers);
|
||||
* old saves without the section regenerate from defaults. */
|
||||
if (m_npcPopulationSystem &&
|
||||
saveData.contains("npcWeekSchedules"))
|
||||
m_npcPopulationSystem->deserializeWeekSchedules(
|
||||
saveData["npcWeekSchedules"]);
|
||||
|
||||
/* Destroy ALL spawned characters so the registry spawn is the
|
||||
* only source of characters. */
|
||||
@@ -2174,7 +2518,6 @@ void EditorApp::loadGame(const std::string &slotPath)
|
||||
saveData["luaData"]);
|
||||
}
|
||||
|
||||
m_currentBaseScene = baseScene;
|
||||
m_playTime = saveData["saveGame"].value("playTime", 0.0f);
|
||||
setGamePlayState(GamePlayState::Playing);
|
||||
|
||||
@@ -2183,6 +2526,93 @@ void EditorApp::loadGame(const std::string &slotPath)
|
||||
EventBus::getInstance().send("game_loaded");
|
||||
}
|
||||
|
||||
void EditorApp::loadGame(const std::string &slotPath)
|
||||
{
|
||||
EventBus::getInstance().send("load_game_requested");
|
||||
|
||||
nlohmann::json saveData;
|
||||
if (!SaveLoadSystem::readSaveFile(slotPath, saveData)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Failed to read save file: " + slotPath);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string baseScene = saveData["saveGame"].value("baseScene", "");
|
||||
if (baseScene.empty()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Save file missing base scene reference");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Phase 8.2 follow-up: restore the vehicle snapshot and the
|
||||
* carjack ledger before the base scene loads, so authored cars
|
||||
* land at their saved poses and stolen owners stay carless. */
|
||||
if (saveData.contains("vehicleState"))
|
||||
VehicleStateStore::getInstance().deserialize(
|
||||
saveData["vehicleState"]);
|
||||
else
|
||||
VehicleStateStore::getInstance().clear();
|
||||
if (m_npcTrafficSystem)
|
||||
m_npcTrafficSystem->deserializeStolenCars(
|
||||
saveData.value("stolenVehicles",
|
||||
nlohmann::json::array()));
|
||||
|
||||
clearScene();
|
||||
if (m_interiorNpcSystem)
|
||||
m_interiorNpcSystem->clear();
|
||||
/* The loaded scene replaces the one the player was in: any
|
||||
* generated interior is gone until the re-entry below. */
|
||||
m_activeGeneratedInterior.clear();
|
||||
m_activeGeneratedSourceScene.clear();
|
||||
|
||||
/* loadGame replaces the base scene: deactivate the terrain
|
||||
* BEFORE loading it so TerrainSystem::update() reactivates
|
||||
* with FRESH entity ids (m_active otherwise stays true with a
|
||||
* stale m_terrainEntityId — the graph read-throughs after a
|
||||
* reload then see a dead entity, as the week-revision probe
|
||||
* exposed). */
|
||||
if (m_terrainSystem)
|
||||
m_terrainSystem->deactivate();
|
||||
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
if (!serializer.loadFromFile(baseScene, m_uiSystem.get())) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Failed to load base scene: " +
|
||||
serializer.getLastError());
|
||||
return;
|
||||
}
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
prefabSys.resolveInstances();
|
||||
applyVehicleStates(baseScene);
|
||||
SceneScriptSystem::loadPendingScripts(m_world);
|
||||
SceneScriptSystem::sendSceneLoaded(baseScene);
|
||||
|
||||
/* Phase 8.2: the BASE scene is authoritative for the world, but
|
||||
* a save taken inside a generated interior carries its world
|
||||
* snapshot in the marker. That snapshot must be restored on the
|
||||
* interior, not on the exterior — and the streamed town may need
|
||||
* a few frames to rebuild the building and register its plan, so
|
||||
* the state restore is deferred until the interior resolves. */
|
||||
m_currentBaseScene = baseScene;
|
||||
m_playTime = saveData["saveGame"].value("playTime", 0.0f);
|
||||
setGamePlayState(GamePlayState::Playing);
|
||||
if (m_gameMode == GameMode::Game &&
|
||||
saveData["saveGame"].contains("interior") &&
|
||||
saveData["saveGame"]["interior"].is_object()) {
|
||||
m_pendingSavedGame = saveData;
|
||||
m_pendingSaveSlot = slotPath;
|
||||
m_pendingInteriorReentry = true;
|
||||
m_pendingInteriorReentryFrames = 0;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game load: re-entering the generated interior " +
|
||||
saveData["saveGame"]["interior"].value(
|
||||
"scene", std::string()));
|
||||
return;
|
||||
}
|
||||
|
||||
applyLoadedSaveState(saveData, slotPath);
|
||||
}
|
||||
|
||||
void EditorApp::setupECS()
|
||||
{
|
||||
// Register components
|
||||
@@ -2220,6 +2650,12 @@ void EditorApp::setupECS()
|
||||
|
||||
// Register Primitive and TriangleBuffer components
|
||||
m_world.component<PrimitiveComponent>();
|
||||
|
||||
/* Phase 8.2 follow-up: player-touched vehicle persistence and
|
||||
* carjack ownership. */
|
||||
m_world.component<VehicleOwnershipComponent>();
|
||||
m_world.component<PlayerTouchedVehicleTag>();
|
||||
m_world.component<InteriorNpcTag>();
|
||||
m_world.component<TriangleBufferComponent>();
|
||||
|
||||
// Register CharacterSlots component
|
||||
@@ -2511,6 +2947,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
|
||||
/* A queued scene switch runs before anything else so systems
|
||||
* below already see the new scene. */
|
||||
processPendingInteriorReentry();
|
||||
processPendingSceneSwitch();
|
||||
stepProfile("sceneSwitch");
|
||||
|
||||
@@ -2631,6 +3068,15 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
}
|
||||
stepProfile("npcPopulation");
|
||||
|
||||
/* A save/load can resolve the interior document before the
|
||||
* population registry is rebuilt: retry any NPCs whose records
|
||||
* were missing now that the population update has run. */
|
||||
if (m_interiorNpcRetryFrames > 0 &&
|
||||
m_interiorNpcSystem) {
|
||||
--m_interiorNpcRetryFrames;
|
||||
populateInteriorNpcs();
|
||||
}
|
||||
|
||||
/* --- NPC vehicle traffic (demo-taxi phase 5.4/6.A): fleet
|
||||
* seeding, clock-pure symbolic drive legs, distance-gated
|
||||
* spawn/despawn. Runs after the population (needs the
|
||||
@@ -2641,7 +3087,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
* the registry position at the last symbolic car position
|
||||
* for one frame (motion already ran with the NPC still
|
||||
* suspended) instead of the pure walk-layer position --- */
|
||||
if (m_npcTrafficSystem) {
|
||||
if (m_npcTrafficSystem && m_activeGeneratedInterior.empty()) {
|
||||
m_npcTrafficSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
stepProfile("npcTraffic");
|
||||
@@ -2658,7 +3104,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
* symbolic motion update so it can override suspended
|
||||
* NPCs, and before the detail tier so its drive targets
|
||||
* are fresh) --- */
|
||||
if (m_npcTaxiSystem) {
|
||||
if (m_npcTaxiSystem && m_activeGeneratedInterior.empty()) {
|
||||
m_npcTaxiSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
stepProfile("npcTaxi");
|
||||
@@ -2671,7 +3117,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
* frame). A despawn persists the entity's live position
|
||||
* into the registry record; the next frame's pure
|
||||
* symbolic evaluation overwrites it. --- */
|
||||
if (m_npcDetailSystem) {
|
||||
if (m_npcDetailSystem && m_activeGeneratedInterior.empty()) {
|
||||
m_npcDetailSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
stepProfile("npcDetail");
|
||||
@@ -2679,7 +3125,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
/* --- NPC local avoidance (phase 4.9.3): halts blocked
|
||||
* pedestrians; runs right after the detail tier so halts
|
||||
* apply to freshly-driven targets --- */
|
||||
if (m_npcAvoidanceSystem) {
|
||||
if (m_npcAvoidanceSystem && m_activeGeneratedInterior.empty()) {
|
||||
m_npcAvoidanceSystem->update();
|
||||
}
|
||||
stepProfile("npcAvoidance");
|
||||
@@ -2828,12 +3274,15 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
* 4.7.7) — same post-physics-sync timing as the driver
|
||||
* pin above. */
|
||||
if (m_npcTaxiSystem) {
|
||||
m_npcTaxiSystem->postPhysicsUpdate();
|
||||
if (m_npcTaxiSystem && m_activeGeneratedInterior.empty())
|
||||
m_npcTaxiSystem->postPhysicsUpdate();
|
||||
}
|
||||
/* NPC traffic drivers pinned to their seats (demo-taxi
|
||||
* phase 5.4) — same post-physics-sync timing. */
|
||||
if (m_npcTrafficSystem) {
|
||||
m_npcTrafficSystem->postPhysicsUpdate();
|
||||
if (m_npcTrafficSystem &&
|
||||
m_activeGeneratedInterior.empty())
|
||||
m_npcTrafficSystem->postPhysicsUpdate();
|
||||
}
|
||||
if (m_playerControllerSystem) {
|
||||
m_playerControllerSystem->postPhysicsUpdate(
|
||||
@@ -3433,34 +3882,50 @@ void EditorApp::locateResources()
|
||||
std::filesystem::current_path().string());
|
||||
}
|
||||
|
||||
/* Try multiple relative paths for characters to handle different
|
||||
* working directories (build root vs binary subdirectory) */
|
||||
struct CharPathPair {
|
||||
const char *male;
|
||||
const char *female;
|
||||
/* Characters live in one directory per age/sex stem: adult keeps
|
||||
* `male`/`female`; new bands use e.g. `baby-male`, `child-female`.
|
||||
* Register every subdirectory as its own flat resource location so
|
||||
* mesh names remain `male_Face.mesh` rather than being prefixed by
|
||||
* the directory path. */
|
||||
const char *charRoots[] = {
|
||||
"./characters",
|
||||
"../../../characters",
|
||||
"../../characters",
|
||||
"../characters",
|
||||
};
|
||||
CharPathPair charPaths[] = {
|
||||
{ "./characters/male", "./characters/female" },
|
||||
{ "../../../characters/male", "../../../characters/female" },
|
||||
{ "../../characters/male", "../../characters/female" },
|
||||
{ "../characters/male", "../characters/female" },
|
||||
};
|
||||
|
||||
bool added = false;
|
||||
for (const auto &pair : charPaths) {
|
||||
if (std::filesystem::exists(pair.male)) {
|
||||
for (const char *root : charRoots) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_directory(root, ec))
|
||||
continue;
|
||||
std::vector<std::filesystem::path> dirs;
|
||||
for (const auto &entry :
|
||||
std::filesystem::directory_iterator(root, ec)) {
|
||||
if (entry.is_directory(ec))
|
||||
dirs.push_back(entry.path());
|
||||
}
|
||||
std::sort(dirs.begin(), dirs.end());
|
||||
if (dirs.empty()) {
|
||||
/* Legacy flat layout. */
|
||||
Ogre::ResourceGroupManager::getSingleton()
|
||||
.addResourceLocation(pair.male, "FileSystem",
|
||||
"Characters", false, true);
|
||||
Ogre::ResourceGroupManager::getSingleton()
|
||||
.addResourceLocation(pair.female, "FileSystem",
|
||||
.addResourceLocation(root, "FileSystem",
|
||||
"Characters", false, true);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Characters resource location added: " +
|
||||
Ogre::String(pair.male));
|
||||
Ogre::String(root));
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
for (const auto &dir : dirs) {
|
||||
Ogre::ResourceGroupManager::getSingleton()
|
||||
.addResourceLocation(dir.string(), "FileSystem",
|
||||
"Characters", false, true);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Characters resource location added: " +
|
||||
dir.string());
|
||||
}
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
if (!added) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <flecs.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include "lua/LuaState.hpp"
|
||||
@@ -17,6 +18,8 @@
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
class GeneratedInteriorService;
|
||||
class InteriorNpcSystem;
|
||||
class VehicleSystem;
|
||||
class AiDriverSystem;
|
||||
class ZoneSystem;
|
||||
@@ -167,6 +170,11 @@ struct SceneSwitchOptions {
|
||||
* When set, the player character is teleported to that entity's
|
||||
* transform (takes precedence over position/rotation). */
|
||||
Ogre::String targetEntityName;
|
||||
|
||||
/* Phase 8.2: an in-memory scene document to load instead of a
|
||||
* file (generated interiors). Null = load from scenePath. The
|
||||
* path stays the display/bookkeeping name. */
|
||||
nlohmann::json sceneDocument;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -197,6 +205,7 @@ public:
|
||||
/* Pin the per-frame delta (seconds) for deterministic headless
|
||||
* tests; 0 (default) uses the wall-clock delta. */
|
||||
void setFixedDeltaTime(float dt) { m_fixedDeltaTime = dt; }
|
||||
float getFixedDeltaTime() const { return m_fixedDeltaTime; }
|
||||
|
||||
// OgreBites::InputListener overrides
|
||||
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
|
||||
@@ -486,6 +495,35 @@ public:
|
||||
{
|
||||
return m_doorSystem.get();
|
||||
}
|
||||
GeneratedInteriorService *getGeneratedInteriorService() const
|
||||
{
|
||||
return m_generatedInteriorService.get();
|
||||
}
|
||||
InteriorNpcSystem *getInteriorNpcSystem() const
|
||||
{
|
||||
return m_interiorNpcSystem.get();
|
||||
}
|
||||
/* Phase 8.2: non-empty while the player is inside a generated
|
||||
* interior ("generated:<gridUid>")); the base scene stays the
|
||||
* exterior one for saves. */
|
||||
bool isInsideGeneratedInterior() const
|
||||
{
|
||||
return !m_activeGeneratedInterior.empty();
|
||||
}
|
||||
const Ogre::String &getActiveGeneratedInterior() const
|
||||
{
|
||||
return m_activeGeneratedInterior;
|
||||
}
|
||||
const Ogre::String &getActiveGeneratedSourceScene() const
|
||||
{
|
||||
return m_activeGeneratedSourceScene;
|
||||
}
|
||||
/* The scene saves are anchored to (the exterior while inside a
|
||||
* generated interior). */
|
||||
const Ogre::String &getCurrentBaseScene() const
|
||||
{
|
||||
return m_currentBaseScene;
|
||||
}
|
||||
Ogre::ImGuiOverlay *getImGuiOverlay() const
|
||||
{
|
||||
return m_imguiOverlay;
|
||||
@@ -543,6 +581,8 @@ private:
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
std::unique_ptr<DoorSystem> m_doorSystem;
|
||||
std::unique_ptr<GeneratedInteriorService> m_generatedInteriorService;
|
||||
std::unique_ptr<InteriorNpcSystem> m_interiorNpcSystem;
|
||||
std::unique_ptr<StandaloneDoorSystem> m_standaloneDoorSystem;
|
||||
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
|
||||
std::unique_ptr<ItemSystem> m_itemSystem;
|
||||
@@ -596,6 +636,25 @@ private:
|
||||
Ogre::String m_pendingSceneSwitchPath;
|
||||
SceneSwitchOptions m_pendingSceneSwitchOptions;
|
||||
|
||||
/* Phase 8.2: deferred load-game re-entry into a generated
|
||||
* interior. The save snapshot is applied only after the interior
|
||||
* switch, so runtime entities and spawns land in the interior. */
|
||||
bool m_pendingInteriorReentry = false;
|
||||
int m_pendingInteriorReentryFrames = 0;
|
||||
Ogre::String m_pendingSaveSlot;
|
||||
nlohmann::json m_pendingSavedGame;
|
||||
nlohmann::json m_applySaveStateAfterSwitch;
|
||||
|
||||
/* Phase 8.2: the generated interior currently loaded (empty while
|
||||
* a normal file scene is active). The BASE scene keeps pointing
|
||||
* at the exterior scene for saves. */
|
||||
Ogre::String m_activeGeneratedInterior;
|
||||
/* Scene ambient saved on interior entry and restored on exit
|
||||
* (generated interiors declare their own ambient). */
|
||||
Ogre::ColourValue m_interiorSavedAmbient;
|
||||
bool m_interiorSavedAmbientValid = false;
|
||||
Ogre::String m_activeGeneratedSourceScene;
|
||||
|
||||
/* Grounding watchdog: after a game-mode scene switch the terrain
|
||||
* physics colliders may not exist yet; keep the player character
|
||||
* at or above the terrain surface until it reports a floor. */
|
||||
@@ -652,6 +711,20 @@ private:
|
||||
double m_profBatchesAccum = 0.0;
|
||||
|
||||
void processPendingSceneSwitch();
|
||||
/* Phase 8.2: deferred load-game re-entry into a generated
|
||||
* interior (waits for the streamed town to register the plan). */
|
||||
void processPendingInteriorReentry();
|
||||
void applyLoadedSaveState(const nlohmann::json &saveData,
|
||||
const std::string &slotPath);
|
||||
/* Phase 8.2 follow-up: player-touched vehicle persistence. */
|
||||
void captureVehicleStates();
|
||||
/* Phase 8.2 follow-up: schedule-driven NPCs in a generated
|
||||
* interior (the exterior detail tier skips indoor NPCs). */
|
||||
void populateInteriorNpcs();
|
||||
/* Deferred retry after a save/load: the population registry is
|
||||
* rebuilt one frame after the interior document is switched in. */
|
||||
int m_interiorNpcRetryFrames = 0;
|
||||
void applyVehicleStates(const Ogre::String &scenePath);
|
||||
bool performSceneSwitch(const Ogre::String &scenePath,
|
||||
const SceneSwitchOptions &opts);
|
||||
void destroySceneEntities();
|
||||
|
||||
@@ -163,6 +163,8 @@ const char* CellGridComponent::getFlagName(uint64_t flag)
|
||||
case CellFlags::IntWindowXPos: return "Int Window X+";
|
||||
case CellFlags::IntWindowZPos: return "Int Window Z+";
|
||||
case CellFlags::IntWindowZNeg: return "Int Window Z-";
|
||||
case CellFlags::StairWell: return "Stairwell";
|
||||
case CellFlags::ExteriorStair: return "Exterior Stair";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -207,5 +209,7 @@ std::vector<std::pair<uint64_t, const char*>> CellGridComponent::getAllFlags()
|
||||
{ CellFlags::IntWindowXPos, "Int Window X+" },
|
||||
{ CellFlags::IntWindowZPos, "Int Window Z+" },
|
||||
{ CellFlags::IntWindowZNeg, "Int Window Z-" },
|
||||
{ CellFlags::StairWell, "Stairwell" },
|
||||
{ CellFlags::ExteriorStair, "Exterior Stair" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,7 +54,24 @@ namespace CellFlags {
|
||||
constexpr uint64_t IntWindowXPos = 1ULL << 23;
|
||||
constexpr uint64_t IntWindowZPos = 1ULL << 24;
|
||||
constexpr uint64_t IntWindowZNeg = 1ULL << 25;
|
||||
|
||||
|
||||
// Phase 7.1: a stair cell. A cell carrying this flag hosts a
|
||||
// straight-flight or dogleg staircase rising from its own floor
|
||||
// (level y) to the floor above (level y+1): the generator cuts
|
||||
// the cell's own ceiling and the floor of the cell directly
|
||||
// above, encloses the opening with a slab-edge ring and fills it
|
||||
// with stepped boxes sized within the character's climb limits.
|
||||
// A stairwell column is consecutive flagged cells at the same
|
||||
// (x, z) on consecutive levels.
|
||||
constexpr uint64_t StairWell = 1ULL << 26;
|
||||
|
||||
// Phase 7.4: an exterior stair. On an upper-floor boundary cell
|
||||
// carrying an exterior door on one side, the generator builds an
|
||||
// outside walkway landing in front of the door and a straight
|
||||
// flight descending along that facade to the ground level
|
||||
// (motel-style external stairs). Does not cut any floors.
|
||||
constexpr uint64_t ExteriorStair = 1ULL << 27;
|
||||
|
||||
// Masks
|
||||
constexpr uint64_t AllExternalWalls = WallXNeg | WallXPos | WallZPos | WallZNeg;
|
||||
constexpr uint64_t AllInternalWalls = IntWallXNeg | IntWallXPos | IntWallZPos | IntWallZNeg;
|
||||
@@ -144,6 +161,16 @@ struct CellGridDoorConfig {
|
||||
std::string actionName;
|
||||
std::string sceneSwitchPath; // F1 (empty = normal swinging door)
|
||||
std::string sceneSwitchTarget;
|
||||
/* Phase 8.1: pre-wired return pose in the destination scene (used
|
||||
* when sceneSwitchTarget is empty). Filled by the generated
|
||||
* interior builder for the exit doors (each entrance doorway is
|
||||
* paired with its exterior approach pose). Serialized only when
|
||||
* hasSceneSwitchPose is set. */
|
||||
bool hasSceneSwitchPose = false;
|
||||
float sceneSwitchPosX = 0.0f;
|
||||
float sceneSwitchPosY = 0.0f;
|
||||
float sceneSwitchPosZ = 0.0f;
|
||||
float sceneSwitchYawDeg = 0.0f;
|
||||
bool disabled = false; // spawn no door entity for this doorway
|
||||
|
||||
// Persistence / locking (F6)
|
||||
@@ -225,7 +252,10 @@ struct CellGridComponent {
|
||||
// Roof texture rectangles
|
||||
std::string roofTopRectName;
|
||||
std::string roofSideRectName;
|
||||
|
||||
|
||||
// Phase 7.1: named rect for stair treads (empty = floorRectName)
|
||||
std::string stairRectName;
|
||||
|
||||
// Door settings - one door entity is spawned per unique doorway
|
||||
// (deduplicated across the two cells sharing a door edge) on the same
|
||||
// condition as door frames (any of the 8 door cell flags).
|
||||
|
||||
@@ -51,6 +51,22 @@ struct CharacterComponent {
|
||||
float floorCheckDistance = 2.0f;
|
||||
bool useGravity = true;
|
||||
|
||||
/* Vertical dynamics (phase 8.4): game-feel gravity multiplier and a
|
||||
* terminal velocity cap. The cap keeps the per-step travel below
|
||||
* the capsule diameter so fast falls cannot tunnel through thin
|
||||
* streamed terrain colliders; the character body also uses linear
|
||||
* cast motion quality (see CharacterSystem). */
|
||||
float gravityScale = 1.35f;
|
||||
float maxFallSpeed = 25.0f;
|
||||
|
||||
/* Controller-level step-up (phase 8.4): when a grounded character
|
||||
* is blocked by a STATIC ledge no higher than this (curbs,
|
||||
* sidewalks, thresholds, low platforms), CharacterSystem lifts the
|
||||
* capsule onto it. Stairs are unaffected: the capsule already
|
||||
* climbs their steps (the horizontal move is not blocked), so the
|
||||
* assist only fires on a blocked move. 0 disables the assist. */
|
||||
float maxStepHeight = 0.4f;
|
||||
|
||||
/* Ground contact state from JPH::Character::IsSupported(), refreshed
|
||||
* by CharacterSystem every frame. Runtime only — used to tell
|
||||
* "floating/swimming" apart from "standing on the bottom in shallow
|
||||
|
||||
@@ -64,6 +64,13 @@ struct DoorComponent {
|
||||
// the scene switch fires once the door is fully open (F1)
|
||||
std::string sceneSwitchPath; // Target scene path (empty = disabled)
|
||||
std::string sceneSwitchTarget; // Teleport target entity name
|
||||
/* Phase 8.2: pre-wired return pose in the target scene (generated
|
||||
* interior exit doors); used when sceneSwitchTarget is empty. */
|
||||
bool sceneSwitchHasPose = false;
|
||||
float sceneSwitchPosX = 0.0f;
|
||||
float sceneSwitchPosY = 0.0f;
|
||||
float sceneSwitchPosZ = 0.0f;
|
||||
float sceneSwitchYawDeg = 0.0f;
|
||||
|
||||
// F1: a scene-switch door first swings open; DoorSystem fires the
|
||||
// switch (and the "door_scene_switch" event) only when the leaf
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Tag for the temporary NPC entities spawned inside a generated
|
||||
* interior from the population schedule (demo-taxi phase 8.2
|
||||
* follow-up). They are idle placeholders for v1: no behavior tree, no
|
||||
* scheduling and no interior pathfinding. Never saved — the interior
|
||||
* is regenerated from the plan on the next visit, and the population
|
||||
* registry keeps the exterior symbolic state.
|
||||
*/
|
||||
struct InteriorNpcTag {
|
||||
};
|
||||
@@ -44,6 +44,13 @@ struct PoiComponent {
|
||||
float closeHour = -1.0f;
|
||||
/** Stat effects offered per in-game hour (stat -> delta). */
|
||||
std::unordered_map<std::string, double> statEffects;
|
||||
/** Planner domain and owning-building identity (6.8.12):
|
||||
* domainValue 0 = Exterior, 1 = Interior. Interior POIs approach
|
||||
* through the building entry; address is the authoritative
|
||||
* fallback key when building is -1. */
|
||||
int domainValue = 0;
|
||||
Ogre::String address;
|
||||
int building = -1;
|
||||
|
||||
/** Activation: kind as int mirroring PoiDatabase::ActivationKind
|
||||
* (0 none, 1 manual, 2 onApproach, 3 onArrive), behaviorTree
|
||||
@@ -59,7 +66,8 @@ struct PoiComponent {
|
||||
|
||||
PoiDatabase::Domain domain() const
|
||||
{
|
||||
return PoiDatabase::Domain::Exterior;
|
||||
return domainValue == 1 ? PoiDatabase::Domain::Interior :
|
||||
PoiDatabase::Domain::Exterior;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -184,4 +184,25 @@ struct VehicleComponent {
|
||||
bool constraintCreated = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Runtime ownership of a vehicle (phase 8.2 follow-up).
|
||||
*
|
||||
* Set when the player takes a car from the NPC traffic fleet: ownerId is
|
||||
* the driver's registry id (0 = unowned / scene-authored). The car
|
||||
* leaves the fleet and the owner walks for the rest of the in-game day;
|
||||
* the next day the fleet assigns a fresh car. Never written to scene
|
||||
* files — VehicleStateStore carries it in save games.
|
||||
*/
|
||||
struct VehicleOwnershipComponent {
|
||||
uint64_t ownerId = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tag: the player has entered this vehicle at least once. Its last pose
|
||||
* is snapshotted by VehicleStateStore on scene switches / saves and
|
||||
* restored when the owning scene loads.
|
||||
*/
|
||||
struct PlayerTouchedVehicleTag {
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_VEHICLE_HPP
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- parallel builds do not work
|
||||
- Water:
|
||||
- sometimes when entering shallow water, characters float above water playing swimming animation.
|
||||
- sometimes in shallow water characters start switch between swimming/walking animations, looks like hysteresis prevention needs to be tweaked and more tests added.
|
||||
@@ -148,6 +148,10 @@ target_include_directories(demoTaxi PRIVATE
|
||||
)
|
||||
|
||||
# Stage the standalone runtime next to the executable (see header comment).
|
||||
# Link dependencies make edits to the menu/config re-run the POST_BUILD.
|
||||
set_property(TARGET demoTaxi APPEND PROPERTY LINK_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/startup_menu.json"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/stage_runtime.cmake")
|
||||
add_custom_command(TARGET demoTaxi POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DDEMO_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1205,6 +1205,10 @@
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
"sceneScript": {
|
||||
"inlineScript": "-- Phase 6.8.9/6.8.14: declare the demo's stat modules and emergency thresholds on every scene load (idempotent).\nlocal rules = require('character_stats_rules')\nrules.define()\n",
|
||||
"scriptPath": ""
|
||||
},
|
||||
"playerController": {
|
||||
"actuatorColor": [
|
||||
0.0,
|
||||
|
||||
@@ -49,6 +49,24 @@ function M.define()
|
||||
ecs.stats.define{ name = "health", default = 100, min = 0, max = 100 }
|
||||
ecs.stats.define{ name = "money", default = 0, min = 0 }
|
||||
ecs.stats.define{ name = "stress", default = 0, min = 0, max = 100 }
|
||||
-- 6.8.14: register the module's emergency thresholds with the
|
||||
-- engine (data-driven; the store listener triggers on health
|
||||
-- CHANGE events, never polling). discharge_at defaults above
|
||||
-- the trigger so an NPC cannot oscillate at the threshold.
|
||||
if ecs.emergency and ecs.emergency.set_thresholds then
|
||||
ecs.emergency.set_thresholds{
|
||||
health_below = M.emergency.hospitalBelowHealth,
|
||||
discharge_at = M.emergency.dischargeHealth,
|
||||
}
|
||||
end
|
||||
-- Schedule completion uses location-specific DSL effects first;
|
||||
-- when a location has none, it falls back to these Lua activity
|
||||
-- rules. The engine never grows a second behavior runtime.
|
||||
if ecs.stats.set_activity_handler then
|
||||
ecs.stats.set_activity_handler(function(id, activity, hours)
|
||||
M.apply_activity(id, activity, hours)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply one completed schedule activity's effects for `hours`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user