chore:模型调整
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
"""Compare dimensions of new GLB assets with matching project FBX assets."""
|
||||
|
||||
from pathlib import Path
|
||||
import csv
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
ASSET_ROOT = PROJECT / "docs" / "asset"
|
||||
NEW_ROOT = next(path for path in ASSET_ROOT.iterdir() if path.is_dir() and any(path.rglob("*.glb")))
|
||||
REFERENCE_ROOT = PROJECT / "assets" / "Models"
|
||||
OUTPUT = PROJECT / "tools" / "new_model_scale_comparison.csv"
|
||||
|
||||
|
||||
def clear_scene():
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
|
||||
|
||||
def dimensions(path: Path, extension: str):
|
||||
clear_scene()
|
||||
if extension == ".glb":
|
||||
bpy.ops.import_scene.gltf(filepath=str(path))
|
||||
else:
|
||||
bpy.ops.import_scene.fbx(filepath=str(path))
|
||||
points = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type == "MESH":
|
||||
points.extend(obj.matrix_world @ Vector(corner) for corner in obj.bound_box)
|
||||
low = [min(point[i] for point in points) for i in range(3)]
|
||||
high = [max(point[i] for point in points) for i in range(3)]
|
||||
return tuple(round(high[i] - low[i], 5) for i in range(3))
|
||||
|
||||
|
||||
references = {path.stem: path for path in REFERENCE_ROOT.rglob("*.fbx")}
|
||||
rows = []
|
||||
for glb in sorted(NEW_ROOT.rglob("*.glb")):
|
||||
reference = references.get(glb.stem)
|
||||
if reference is None:
|
||||
continue
|
||||
glb_size = dimensions(glb, ".glb")
|
||||
fbx_size = dimensions(reference, ".fbx")
|
||||
ratios = [fbx_size[i] / glb_size[i] for i in range(3) if glb_size[i] > 0 and fbx_size[i] > 0]
|
||||
rows.append((glb.stem, *glb_size, *fbx_size, round(sum(ratios) / len(ratios), 5)))
|
||||
|
||||
with OUTPUT.open("w", newline="", encoding="utf-8-sig") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(("model", "glb_x", "glb_y", "glb_z", "project_fbx_x", "project_fbx_y", "project_fbx_z", "project_to_new_ratio"))
|
||||
writer.writerows(rows)
|
||||
print(f"Wrote {len(rows)} comparisons to {OUTPUT}")
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Batch-convert GLB files to FBX through Blender.
|
||||
|
||||
Run this file with Blender's Python, not the system Python. The script keeps
|
||||
the source folder layout and writes an errors.csv next to the output files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source", required=True, type=Path)
|
||||
parser.add_argument("--destination", required=True, type=Path)
|
||||
parser.add_argument("--overwrite", action="store_true")
|
||||
return parser.parse_args(sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [])
|
||||
|
||||
|
||||
def clear_scene() -> None:
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
for collection in (bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.armatures):
|
||||
for item in list(collection):
|
||||
collection.remove(item)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
source = args.source.resolve()
|
||||
destination = args.destination.resolve()
|
||||
if not source.is_dir():
|
||||
print(f"ERROR: source folder does not exist: {source}")
|
||||
return 2
|
||||
if source == destination:
|
||||
print("ERROR: destination must be different from source")
|
||||
return 2
|
||||
|
||||
glb_files = sorted(source.rglob("*.glb"))
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
errors: list[tuple[str, str]] = []
|
||||
converted = skipped = 0
|
||||
|
||||
for index, input_file in enumerate(glb_files, start=1):
|
||||
output_file = destination / input_file.relative_to(source).with_suffix(".fbx")
|
||||
if output_file.exists() and not args.overwrite:
|
||||
print(f"[{index}/{len(glb_files)}] SKIP {input_file.name}")
|
||||
skipped += 1
|
||||
continue
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
clear_scene()
|
||||
bpy.ops.import_scene.gltf(filepath=str(input_file))
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=str(output_file),
|
||||
use_selection=False,
|
||||
object_types={"EMPTY", "CAMERA", "LIGHT", "ARMATURE", "MESH", "OTHER"},
|
||||
use_mesh_modifiers=True,
|
||||
add_leaf_bones=False,
|
||||
path_mode="COPY",
|
||||
embed_textures=True,
|
||||
)
|
||||
converted += 1
|
||||
print(f"[{index}/{len(glb_files)}] OK {input_file.name}")
|
||||
except Exception as exc: # continue converting the remaining assets
|
||||
errors.append((str(input_file), repr(exc)))
|
||||
print(f"[{index}/{len(glb_files)}] FAIL {input_file.name}: {exc}")
|
||||
|
||||
if errors:
|
||||
error_log = destination / "errors.csv"
|
||||
with error_log.open("w", newline="", encoding="utf-8-sig") as handle:
|
||||
writer = csv.writer(handle)
|
||||
writer.writerow(("source_file", "error"))
|
||||
writer.writerows(errors)
|
||||
print(f"Error log: {error_log}")
|
||||
print(f"Finished: {converted} converted, {skipped} skipped, {len(errors)} failed.")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
param(
|
||||
[string]$Source,
|
||||
[string]$Destination,
|
||||
[switch]$Overwrite
|
||||
)
|
||||
|
||||
$Blender = "E:\Program Files\Blender Foundation\Blender 5.2\blender.exe"
|
||||
$Script = Join-Path $PSScriptRoot "convert_glb_to_fbx.py"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Source)) {
|
||||
$AssetRoot = Join-Path (Split-Path $PSScriptRoot -Parent) "docs\asset"
|
||||
$Source = Get-ChildItem -LiteralPath $AssetRoot -Directory |
|
||||
Where-Object { @(Get-ChildItem -LiteralPath $_.FullName -Recurse -File -Filter "*.glb").Count -gt 0 } |
|
||||
Select-Object -First 1 -ExpandProperty FullName
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Destination)) { $Destination = "$Source`_fbx" }
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Blender)) { throw "Blender was not found: $Blender" }
|
||||
if (-not (Test-Path -LiteralPath $Source)) { throw "Source folder was not found: $Source" }
|
||||
|
||||
$Arguments = @("--background", "--python", $Script, "--", "--source", $Source, "--destination", $Destination)
|
||||
if ($Overwrite) { $Arguments += "--overwrite" }
|
||||
|
||||
& $Blender @Arguments
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,102 @@
|
||||
model,glb_x,glb_y,glb_z,project_fbx_x,project_fbx_y,project_fbx_z,project_to_new_ratio
|
||||
SM_Background_City_02,0.69548,2.21799,7.25709,14.29015,0.0,15.37664,11.33301
|
||||
SM_Bld_Apartments_01,4.68933,3.14799,6.58278,1.08974,1.31511,2.31951,0.33417
|
||||
SM_Bld_Apartments_02,685.05756,507.09831,1023.39322,1.09103,1.25694,2.46071,0.00216
|
||||
SM_Bld_Apartments_03,0.11738,0.09717,0.14042,1.08236,1.34601,2.46071,13.53234
|
||||
SM_Bld_Apartments_04,863.37424,1090.10559,1654.52451,1.06326,1.22201,2.13264,0.00121
|
||||
SM_Bld_Apartments_Block_01,25.34438,23.36829,22.96318,2.70139,1.55609,2.46071,0.09345
|
||||
SM_Bld_Apartments_Block_02,18.94554,17.62445,17.04079,3.10854,2.32077,2.46071,0.14672
|
||||
SM_Bld_Church_01,1381.84839,1884.85574,1934.2692,1.26475,1.861,2.64343,0.00109
|
||||
SM_Bld_CityHall_01,1.28757,1.04367,1.08939,2.47677,2.18277,1.37282,1.7584
|
||||
SM_Bld_House_02,475.6608,884.38977,644.3027,0.78916,1.18354,0.81988,0.00142
|
||||
SM_Bld_House_03,6.36,7.45,5.85,1.30453,1.5264,1.09218,0.1989
|
||||
SM_Bld_House_05,7.7198,7.37393,10.36916,1.70929,0.99749,1.09207,0.154
|
||||
SM_Bld_House_07,1132.9505,2009.90247,990.99037,1.75699,1.68834,1.33679,0.00125
|
||||
SM_Bld_House_08,370.99999,256.02248,333.54853,1.40633,2.41373,1.22004,0.00563
|
||||
SM_Bld_House_10,368.8779,214.66895,290.11153,1.54676,1.44984,1.4634,0.00533
|
||||
SM_Bld_House_11,6.86464,6.03142,8.04465,2.4441,2.37924,1.76332,0.32324
|
||||
SM_Bld_OfficeOctagon_01,103.77404,103.81444,235.11004,1.6757,1.6757,2.79256,0.01472
|
||||
SM_Bld_OfficeOld_Large_01,5.38043,3.44203,5.37686,1.6334,1.6334,2.55487,0.41776
|
||||
SM_Bld_OfficeRound_01,25.15329,27.18591,56.368,1.47989,1.4799,2.59303,0.05309
|
||||
SM_Bld_OfficeSquare_01,108.28931,60.00002,326.79121,1.22006,1.22006,2.88175,0.01347
|
||||
SM_Bld_Shop_01,13.54689,11.39883,5.07535,1.36853,1.2948,1.11662,0.14487
|
||||
SM_Bld_Shop_02,113.9176,98.98986,52.56013,1.93887,1.34646,1.07373,0.01702
|
||||
SM_Bld_Shop_03,1574.65564,1559.59204,786.92122,2.09754,2.2047,1.06234,0.00137
|
||||
SM_Bld_Station_01,2941.36243,2393.23535,2560.70125,4.59497,1.67788,1.69871,0.00098
|
||||
SM_Bld_Station_02,1.91491,2.13875,2.66549,1.62324,1.62323,1.69871,0.74798
|
||||
SM_Env_Fence_01,4.20314,0.3206,2.73405,0.43742,0.01741,0.23644,0.08162
|
||||
SM_Env_Fence_02,0.60854,9.84975,6.33737,0.43641,0.06116,0.25433,0.25449
|
||||
SM_Env_Generic_Bush_01,6.73399,7.18018,5.23314,0.48032,0.31855,0.2597,0.05511
|
||||
SM_Env_Generic_Bush_02,123.72953,77.31821,60.72777,0.28091,0.29047,0.25134,0.00339
|
||||
SM_Env_Generic_River_Stone_02,0.10468,0.15147,0.07811,0.61023,0.54089,0.10295,3.57281
|
||||
SM_Env_Generic_Rock_04,2.00732,1.99077,1.55727,0.85123,0.74968,0.1652,0.30224
|
||||
SM_Env_Generic_Tree_01,0.6856,0.5899,0.7368,0.54981,0.47615,1.11742,1.0419
|
||||
SM_Env_Generic_Tree_03,0.36783,0.38901,0.56396,1.02748,1.01284,1.08527,2.44045
|
||||
SM_Env_Generic_Tree_04,335.77576,335.60579,576.65575,0.52926,0.48412,0.9674,0.00157
|
||||
SM_Env_Grass_01,1.37798,1.58631,1.80551,0.33247,0.42413,0.31007,0.22679
|
||||
SM_Env_Pool_01,2.0,2.0,1.77263,1.14304,1.14304,0.27115,0.432
|
||||
SM_Env_RoadPatch_01,38.2175,38.94163,1.36702,0.71528,0.47709,0.02005,0.01521
|
||||
SM_Env_WallPark_Corner_01,0.80317,0.79611,0.45142,1.12481,1.12481,0.63994,1.41032
|
||||
SM_Env_WallPark_Corner_02,1.11296,0.49945,0.33097,1.63778,1.11307,0.63994,1.87789
|
||||
SM_Env_WallPark_Straight_01,106.5006,23.27893,53.24705,1.12171,0.26935,0.63994,0.01137
|
||||
SM_Generic_Ground_01,1018.54285,1018.54303,844.66702,3.50151,3.59726,0.16318,0.00239
|
||||
SM_Prop_BBQ_01,279.16141,95.69674,184.67297,0.53033,0.3585,0.45431,0.0027
|
||||
SM_Prop_Billboard_Base_01,44.29373,271.14278,147.36865,0.4614,0.12016,0.37081,0.00446
|
||||
SM_Prop_BusStop_01,3.81107,1.61119,2.525,0.77668,0.30664,0.52219,0.20031
|
||||
SM_Prop_CardboardBox_01,8.71478,7.08694,6.84178,0.34782,0.33854,0.27174,0.04247
|
||||
SM_Prop_Drain_01,19.23742,19.2913,0.66166,0.48729,0.29083,0.06054,0.04397
|
||||
SM_Prop_Dumpster_Closed_01,1.19785,1.97377,1.5135,0.60115,0.41735,0.52524,0.35345
|
||||
SM_Prop_Dumpster_Open_01,175.42211,103.07324,129.3989,0.60115,0.45428,0.89502,0.00492
|
||||
SM_Prop_FireHydrant_01,0.60019,0.51805,1.12728,0.24522,0.23678,0.44447,0.41997
|
||||
SM_Prop_FlagPole_01,161.82344,227.43806,57.60232,0.0603,0.40035,0.4776,0.00347
|
||||
SM_Prop_Flamingo_01,48.91436,16.33452,69.17754,0.30538,0.0908,0.44651,0.00609
|
||||
SM_Prop_Fountain_01,13.906,13.906,8.45385,0.76971,0.66661,0.40239,0.0503
|
||||
SM_Prop_Garden_01,15.37682,15.37682,3.04318,0.40087,0.40087,0.21626,0.04107
|
||||
SM_Prop_Garden_02,1.62948,1.10473,0.07707,0.75537,0.40087,0.2212,1.23218
|
||||
SM_Prop_Helipad_01,10.91023,10.91023,0.23701,0.92692,0.92693,0.06836,0.15278
|
||||
SM_Prop_HotdogCart_01,3.84464,3.84464,3.59613,0.67027,0.5935,0.8584,0.18914
|
||||
SM_Prop_LetterBox_01,0.00998,0.01461,0.04365,0.10155,0.17545,0.46047,10.91113
|
||||
SM_Prop_OutdoorHeader_01,2.96217,2.96217,2.77573,0.353,0.30571,0.67326,0.15498
|
||||
SM_Prop_ParkBench_01,7.28595,3.71581,5.65442,0.45142,0.16979,0.28417,0.05264
|
||||
SM_Prop_PhoneBooth_01,38.87959,37.3667,156.15133,0.79749,0.35382,0.69589,0.01148
|
||||
SM_Prop_Playground_01,7.0331,11.45797,6.56647,0.6233,0.92598,0.58828,0.08634
|
||||
SM_Prop_Playground_02,1.5,2.0,0.75,0.52417,0.52464,0.22678,0.30471
|
||||
SM_Prop_Playground_03,2.73628,6.26311,5.25295,0.28082,0.70537,0.35804,0.09447
|
||||
SM_Prop_Playground_04,2.0,2.0,0.789,0.83021,0.83021,0.22225,0.37063
|
||||
SM_Prop_Pool_Float_Ring_05,0.97817,1.00072,0.82379,0.3303,0.32967,0.19202,0.30007
|
||||
SM_Prop_Potplants_01,11.36936,1.24811,2.17385,0.36419,0.15104,0.18395,0.07922
|
||||
SM_Prop_PowerBox_01,0.39528,0.66,0.75845,0.42847,0.23621,0.40737,0.65966
|
||||
SM_Prop_RoadBump_01,107.00941,11.60036,53.23364,1.28321,0.2008,0.05406,0.01011
|
||||
SM_Prop_Roadcone_01,1.524,1.524,2.54,0.19298,0.19299,0.24189,0.11616
|
||||
SM_Prop_Sign_02,0.24552,1.84776,7.0,0.2689,0.09427,0.74396,0.41751
|
||||
SM_Prop_Sign_IceCream_01,10.31887,10.50033,30.03268,0.42924,0.37173,1.00917,0.03687
|
||||
SM_Prop_Sign_Popcorn_01,1.35837,1.35837,2.0,0.42322,0.42127,0.70028,0.32394
|
||||
SM_Prop_Spa_01,8.79603,8.79603,2.37212,0.81437,0.70527,0.28537,0.09769
|
||||
SM_Prop_Swingset_01,17.69562,18.9807,21.45482,0.86507,0.65536,0.72705,0.0391
|
||||
SM_Prop_TrafficLight_01,1.20177,0.99179,7.41,0.30846,0.36292,1.38417,0.2698
|
||||
SM_Prop_Trashbag_01,2.0,1.67131,1.91278,0.33138,0.33138,0.36199,0.1844
|
||||
SM_Prop_WaterTower_01,1.97973,3.08043,4.21858,0.44216,0.44216,1.05823,0.20591
|
||||
SM_Tile_Bridge_01,4822.71106,998.0307,969.53262,1.79035,4.99597,0.85209,0.00209
|
||||
SM_Tile_Cube_Concrete_01,11.81102,11.81102,1.96851,5.0,5.0,0.43242,0.35545
|
||||
SM_Tile_Cube_Concrete_03,6.00927,5.84295,0.45,5.0,5.0,0.43242,0.8829
|
||||
SM_Tile_Cube_Concrete_04,1.0,1.0,0.0,5.00027,5.00076,0.43147,5.00052
|
||||
SM_Tile_Cube_Grass_01,5.0,5.0,0.13838,5.0,5.0,0.50198,1.87585
|
||||
SM_Tile_Cube_Path_Grass_03,0.84288,0.84628,0.14712,5.0,5.0,0.50198,5.0841
|
||||
SM_Tile_Cube_Road_Concrete_01,2307.99976,1460.00024,48.00039,5.0,5.0,0.43242,0.00487
|
||||
SM_Tile_Cube_Road_Concrete_02,996.85956,1141.18781,21.35264,5.0,5.0,0.43242,0.00988
|
||||
SM_Tile_Cube_Road_Concrete_04,0.90794,0.14309,0.82242,5.0,5.0,0.43242,13.6586
|
||||
SM_Tile_Cube_Road_Grass_07,84.8795,84.85878,15.86925,5.0,5.0,0.50364,0.04986
|
||||
SM_Tile_Hex_Grass_01,1.70759,0.9076,0.20707,5.77417,5.00076,0.49279,3.75706
|
||||
SM_Tile_Ramp_14,1200.00006,555.99994,467.57274,1.79035,5.00005,0.96407,0.00418
|
||||
SM_Veh_Bus_01,11.50354,4.82497,4.84264,1.03079,2.40287,0.88361,0.25669
|
||||
SM_Veh_Car_Ambo_01,2.747,5.6364,3.0023,0.81924,1.41634,0.97278,0.29118
|
||||
SM_Veh_Car_Medium_01,1.94016,4.23,1.92969,0.64088,1.48005,0.73093,0.353
|
||||
SM_Veh_Car_Muscle_01,1.93382,4.55527,1.22209,0.84551,1.66101,0.53417,0.41298
|
||||
SM_Veh_Car_Police_01,3.77022,1.97826,1.55441,0.79713,1.47207,0.71478,0.4718
|
||||
SM_Veh_Car_Sedan_01,5.14178,9.38673,5.15166,0.79713,1.44128,0.59798,0.14155
|
||||
SM_Veh_Car_Small_01,1.8559,3.80523,1.49906,0.70371,1.17695,0.5327,0.34794
|
||||
SM_Veh_Car_Taxi_01,0.13286,0.25675,0.12331,0.79713,1.44128,0.73805,5.86622
|
||||
SM_Veh_Car_Van_01,5.20041,11.17683,4.98729,0.81924,1.38511,0.74,0.14328
|
||||
SM_Veh_Convertable_01,1.90525,0.80117,0.54993,0.83514,1.51832,0.48688,1.07294
|
||||
SM_Veh_Pickup_01,7.51748,2.88886,3.06634,0.88997,1.5441,0.85548,0.31063
|
||||
SM_Veh_Truck_01,0.99336,2.0,1.05006,0.83043,1.91525,1.04004,0.92802
|
||||
|
@@ -0,0 +1,64 @@
|
||||
param([switch]$NoBackup)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Project = Split-Path $PSScriptRoot -Parent
|
||||
$AssetRoot = Join-Path $Project 'docs\asset'
|
||||
$NewRoot = Get-ChildItem -LiteralPath $AssetRoot -Directory |
|
||||
Where-Object { @(Get-ChildItem -LiteralPath $_.FullName -Recurse -File -Filter '*.glb').Count -gt 0 } |
|
||||
Select-Object -First 1
|
||||
if ($null -eq $NewRoot) { throw 'Could not find the new GLB asset folder.' }
|
||||
|
||||
$ScaledRoot = Join-Path $AssetRoot "$($NewRoot.Name)_project_scale_fbx\models"
|
||||
$ReferenceDirs = @((Join-Path $Project 'assets\Models'), (Join-Path $Project 'assets\Model'))
|
||||
$SpecialReferences = @{
|
||||
'BulletTrail' = 'BulletTrail'
|
||||
'SM_Item_Bullet_01' = 'bullet'
|
||||
'SM_Tile_Cube_Water_0' = 'SM_Tile_Cube_Water_01'
|
||||
'test' = 'test'
|
||||
'WEP_98k' = 'Wep_Rifle_98k'
|
||||
'WEP_M4A1 copy' = 'Wep_Rifle_1'
|
||||
'WEP_Sniper_1' = 'Wep_Sniper_01'
|
||||
}
|
||||
|
||||
function Normalize-ModelName([string]$Name) {
|
||||
return (($Name.ToLowerInvariant().ToCharArray() | Where-Object { [char]::IsLetterOrDigit($_) }) -join '')
|
||||
}
|
||||
|
||||
$References = @{}
|
||||
foreach ($Directory in $ReferenceDirs) {
|
||||
Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter '*.fbx' | ForEach-Object {
|
||||
$References[(Normalize-ModelName $_.BaseName)] = $_
|
||||
}
|
||||
}
|
||||
|
||||
$Replacements = foreach ($Glb in Get-ChildItem -LiteralPath $NewRoot.FullName -Recurse -File -Filter '*.glb' | Sort-Object FullName) {
|
||||
$ReferenceName = if ($SpecialReferences.ContainsKey($Glb.BaseName)) { $SpecialReferences[$Glb.BaseName] } else { $Glb.BaseName }
|
||||
$Target = $References[(Normalize-ModelName $ReferenceName)]
|
||||
$Relative = $Glb.FullName.Substring($NewRoot.FullName.Length).TrimStart('\')
|
||||
$Source = Join-Path $ScaledRoot ([IO.Path]::ChangeExtension($Relative, '.fbx'))
|
||||
if ($null -eq $Target) { throw "No project target for: $($Glb.Name)" }
|
||||
if (-not (Test-Path -LiteralPath $Source)) { throw "Scaled FBX missing: $Source" }
|
||||
[PSCustomObject]@{ Source = $Source; Target = $Target.FullName; Reference = $Target.Name }
|
||||
}
|
||||
|
||||
if (@($Replacements).Count -ne 113) { throw "Expected 113 replacements; found $(@($Replacements).Count)." }
|
||||
if (@($Replacements.Target | Select-Object -Unique).Count -ne @($Replacements).Count) { throw 'Multiple new models map to the same project target.' }
|
||||
|
||||
if (-not $NoBackup) {
|
||||
$BackupRoot = Join-Path $AssetRoot ('project_model_fbx_backup_' + (Get-Date -Format 'yyyyMMdd_HHmmss'))
|
||||
New-Item -ItemType Directory -Path $BackupRoot | Out-Null
|
||||
foreach ($Item in $Replacements) {
|
||||
$RelativeTarget = $Item.Target.Substring($Project.Length).TrimStart('\')
|
||||
$Backup = Join-Path $BackupRoot $RelativeTarget
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $Backup -Parent) | Out-Null
|
||||
Copy-Item -LiteralPath $Item.Target -Destination $Backup
|
||||
}
|
||||
}
|
||||
foreach ($Item in $Replacements) {
|
||||
Copy-Item -LiteralPath $Item.Source -Destination $Item.Target -Force
|
||||
}
|
||||
if ($BackupRoot) {
|
||||
$Replacements | Export-Csv -LiteralPath (Join-Path $BackupRoot 'replacement_manifest.csv') -NoTypeInformation -Encoding UTF8
|
||||
}
|
||||
Write-Output "Replaced $(@($Replacements).Count) FBX files."
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Export new GLB assets as FBX, scaled to the matching project asset size.
|
||||
|
||||
The uniform scale is based on the longest bounding-box edge, which preserves
|
||||
each new model's proportions while matching the project's visual scale.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import csv
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
PROJECT = Path(__file__).resolve().parent.parent
|
||||
ASSET_ROOT = PROJECT / "docs" / "asset"
|
||||
NEW_ROOT = next(path for path in ASSET_ROOT.iterdir() if path.is_dir() and any(path.rglob("*.glb")))
|
||||
OUTPUT_ROOT = NEW_ROOT.parent / f"{NEW_ROOT.name}_project_scale_fbx" / "models"
|
||||
REFERENCE_DIRS = (PROJECT / "assets" / "Models", PROJECT / "assets" / "Model")
|
||||
SPECIAL_REFERENCES = {
|
||||
"BulletTrail": "BulletTrail",
|
||||
"SM_Item_Bullet_01": "bullet",
|
||||
"SM_Tile_Cube_Water_0": "SM_Tile_Cube_Water_01",
|
||||
"test": "test",
|
||||
"WEP_98k": "Wep_Rifle_98k",
|
||||
"WEP_M4A1 copy": "Wep_Rifle_1",
|
||||
"WEP_Sniper_1": "Wep_Sniper_01",
|
||||
}
|
||||
|
||||
|
||||
def clear_scene():
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
|
||||
|
||||
def bounds():
|
||||
points = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type == "MESH":
|
||||
points.extend(obj.matrix_world @ Vector(corner) for corner in obj.bound_box)
|
||||
if not points:
|
||||
raise RuntimeError("model has no mesh objects")
|
||||
low = [min(point[i] for point in points) for i in range(3)]
|
||||
high = [max(point[i] for point in points) for i in range(3)]
|
||||
return tuple(high[i] - low[i] for i in range(3))
|
||||
|
||||
|
||||
def load(path: Path):
|
||||
clear_scene()
|
||||
if path.suffix.lower() == ".glb":
|
||||
bpy.ops.import_scene.gltf(filepath=str(path))
|
||||
else:
|
||||
bpy.ops.import_scene.fbx(filepath=str(path))
|
||||
# Cocos Creator's FBX-to-glTF converter writes invalid accessors for
|
||||
# mesh objects that have materials but contain no faces. Blender keeps
|
||||
# those empty mesh objects when importing some GLB files, so remove them.
|
||||
for obj in list(bpy.context.scene.objects):
|
||||
if obj.type == "MESH" and len(obj.data.polygons) == 0:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
def normalized(name: str) -> str:
|
||||
return "".join(char.lower() for char in name if char.isalnum())
|
||||
|
||||
|
||||
references = {}
|
||||
for folder in REFERENCE_DIRS:
|
||||
for file in folder.rglob("*.fbx"):
|
||||
references[normalized(file.stem)] = file
|
||||
|
||||
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
report = []
|
||||
errors = []
|
||||
for glb in sorted(NEW_ROOT.rglob("*.glb")):
|
||||
reference_name = SPECIAL_REFERENCES.get(glb.stem, glb.stem)
|
||||
reference = references.get(normalized(reference_name))
|
||||
try:
|
||||
if reference is None:
|
||||
raise RuntimeError(f"no project reference found for {reference_name}")
|
||||
load(reference)
|
||||
reference_size = bounds()
|
||||
target_length = max(reference_size)
|
||||
|
||||
load(glb)
|
||||
source_size = bounds()
|
||||
source_length = max(source_size)
|
||||
if source_length == 0:
|
||||
raise RuntimeError("model bounding box has zero size")
|
||||
scale = target_length / source_length
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.parent is None:
|
||||
obj.location *= scale
|
||||
obj.scale *= scale
|
||||
|
||||
output = OUTPUT_ROOT / glb.relative_to(NEW_ROOT).with_suffix(".fbx")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=str(output), use_selection=False,
|
||||
object_types={"EMPTY", "CAMERA", "LIGHT", "ARMATURE", "MESH", "OTHER"},
|
||||
use_mesh_modifiers=True, add_leaf_bones=False,
|
||||
path_mode="COPY", embed_textures=True,
|
||||
)
|
||||
report.append((str(glb.relative_to(NEW_ROOT)), reference.name, scale, source_length, target_length))
|
||||
print(f"OK {glb.name}: x{scale:.8g} -> {reference.name}")
|
||||
except Exception as error:
|
||||
errors.append((str(glb), str(error)))
|
||||
print(f"FAIL {glb.name}: {error}")
|
||||
|
||||
with (OUTPUT_ROOT / "scale_report.csv").open("w", newline="", encoding="utf-8-sig") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(("source", "reference", "uniform_scale", "source_longest_edge", "target_longest_edge"))
|
||||
writer.writerows(report)
|
||||
if errors:
|
||||
with (OUTPUT_ROOT / "errors.csv").open("w", newline="", encoding="utf-8-sig") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(("source", "error"))
|
||||
writer.writerows(errors)
|
||||
print(f"Finished: {len(report)} scaled, {len(errors)} failed. Output: {OUTPUT_ROOT}")
|
||||
raise SystemExit(1 if errors else 0)
|
||||
Reference in New Issue
Block a user