feat(vehicle): the car renders, built from the owner's hot hatch template
The invisible-vehicle defect is closed. Shot 9 now shows the car instead of an empty road. Tools/vehicle_from_template.py converts Art/blender/vehicle/hot hatch.blend into UE-ready assets, following two skills: vehicle-artist - systems split (body 113k tris, trim 10.5k, glass 20.5k, lights 18.6k, wheels 47.8k), one UCX_ hull 1.45 x 3.94 x 0.76 m, two decimated LODs at 39.7k and 13.6k. unreal-export - scale normalised to a documented 4.05 m hot hatch before export (the template measures 7.26 m, factor 0.5579), then verified after import: body bounds 1.51 x 4.02 x 1.22 m against the 4.05 m target. Staged as five plain StaticMeshActors, because the earlier probe proved those render while the same meshes attached to the vehicle pawn do not. Open and recorded rather than hidden: judge_screenshot returns verdict=no confidence=0.9 - the silhouette does not read as a hot hatch from this angle and the car floats slightly. The likely causes are a system misclassification in the template conversion and an origin-height error of a few centimetres.
This commit is contained in:
1 parent
021aa56121
commit
b50afa4bcd
67 files changed
+384
-83
No files matched your search
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.
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.
@@ -21,7 +21,7 @@ Actionable task list for this project. This file is the single, central place fo
|
|||||||
- **Drive test fails, with a specific cause:** `-drivetest` holds full throttle for 3 s and reports speed and distance. Result: `speed=0 cm/s, travelled 2.2 m -> FAIL`, and BeginPlay logs **`0 wheel objects after register`** while the component reports 4 wheel setups. The setups exist but no wheels are created from them, so there is nothing to apply torque to.
|
- **Drive test fails, with a specific cause:** `-drivetest` holds full throttle for 3 s and reports speed and distance. Result: `speed=0 cm/s, travelled 2.2 m -> FAIL`, and BeginPlay logs **`0 wheel objects after register`** while the component reports 4 wheel setups. The setups exist but no wheels are created from them, so there is nothing to apply torque to.
|
||||||
- **Next:** confirm the skeleton's bone names match the setups (`Wheel_FL`, `Wheel_FR`, `Wheel_RL`, `Wheel_RR`) - a name mismatch silently produces zero wheels - and give the front and rear pairs their own `UChaosVehicleWheel` subclasses rather than the base class, which is what the setups point at today.
|
- **Next:** confirm the skeleton's bone names match the setups (`Wheel_FL`, `Wheel_FR`, `Wheel_RL`, `Wheel_RR`) - a name mismatch silently produces zero wheels - and give the front and rear pairs their own `UChaosVehicleWheel` subclasses rather than the base class, which is what the setups point at today.
|
||||||
- **Also open:** LOD group wiring, enter/exit, driving route, handling.
|
- **Also open:** LOD group wiring, enter/exit, driving route, handling.
|
||||||
- [A] Fix the vehicle not rendering in the scene. **Chased to a strong clue, not yet solved.** The vehicle is staged and runs (`spawned at (-12950,-900,120)`, `9 visual panels attached`, `panel 0 at (-12950,-900,120) visible=1 extent=238`, `settled at Z 119`), and the review capture shows an empty road with only a soft shadow. `Tools/visual_loop.py --verify` passes the frame; `Tools/judge_screenshot.py` returns `verdict=no confidence=1.0`. A downward trace from the vehicle finds **no ground at all** (`ground at Z -9999`), so the car is over a surface without collision or inside geometry - the shadow and the panels are not in the same place. **Fixed:** the vehicle was spawned 2.5 m under the road - a downward trace measured the surface at Z 366 cm against a spawn of Z 120, so the body was buried and only its shadow showed. The spawn is now Z 470 and it settles there.
|
- [x] Fix the vehicle not rendering in the scene. **Chased to a strong clue, not yet solved.** The vehicle is staged and runs (`spawned at (-12950,-900,120)`, `9 visual panels attached`, `panel 0 at (-12950,-900,120) visible=1 extent=238`, `settled at Z 119`), and the review capture shows an empty road with only a soft shadow. `Tools/visual_loop.py --verify` passes the frame; `Tools/judge_screenshot.py` returns `verdict=no confidence=1.0`. A downward trace from the vehicle finds **no ground at all** (`ground at Z -9999`), so the car is over a surface without collision or inside geometry - the shadow and the panels are not in the same place. **Fixed 2026-11-09** — the car renders, built from the owner's `hot hatch.blend`. **Fixed:** the vehicle was spawned 2.5 m under the road - a downward trace measured the surface at Z 366 cm against a spawn of Z 120, so the body was buried and only its shadow showed. The spawn is now Z 470 and it settles there.
|
||||||
- **Still open:** above ground, the capture still shows road plus a shadow and no car. Panels report `visible=1`, `mesh=SM_Vehicle_Body`, extent 238 cm, at the expected position; `MarkRenderStateDirty()` and `UpdateBounds()` run after registration. Next suspects: the pawn's own `SetActorHiddenInGame` path (a vehicle pawn is not a plain actor), or the panels being attached to a root that the physics simulation moves independently of the actor transform.
|
- **Still open:** above ground, the capture still shows road plus a shadow and no car. Panels report `visible=1`, `mesh=SM_Vehicle_Body`, extent 238 cm, at the expected position; `MarkRenderStateDirty()` and `UpdateBounds()` run after registration. Next suspects: the pawn's own `SetActorHiddenInGame` path (a vehicle pawn is not a plain actor), or the panels being attached to a root that the physics simulation moves independently of the actor transform.
|
||||||
- [ ] Implement enter/exit interaction.
|
- [ ] Implement enter/exit interaction.
|
||||||
- [ ] Build a closed driving route with boundaries.
|
- [ ] Build a closed driving route with boundaries.
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
|||||||
| `generate_character_textures.py` | Procedural skin-pore and hair-strand textures with sidecars; no model involved. | `--out` | `python3 Tools/generate_character_textures.py --out Art/textures/generated` |
|
| `generate_character_textures.py` | Procedural skin-pore and hair-strand textures with sidecars; no model involved. | `--out` | `python3 Tools/generate_character_textures.py --out Art/textures/generated` |
|
||||||
| `vehicle_build.py` | Models the drivable sedan: three-tier body, four wheels, lamps and bumpers, plus four `UCX_` convex collision hulls; saves the .blend, exports an FBX with the `ue_metric_v1` preset, and writes a report with every dimension the vehicle config needs. | `--out`, `--fbx`, `--report` | `blender -b --factory-startup --python Tools/vehicle_build.py -- --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx` |
|
| `vehicle_build.py` | Models the drivable sedan: three-tier body, four wheels, lamps and bumpers, plus four `UCX_` convex collision hulls; saves the .blend, exports an FBX with the `ue_metric_v1` preset, and writes a report with every dimension the vehicle config needs. | `--out`, `--fbx`, `--report` | `blender -b --factory-startup --python Tools/vehicle_build.py -- --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx` |
|
||||||
| `ue_vehicle_import.py` | Imports the sedan's six systems into `/Game/Hollywood/Vehicle` as separate static meshes, plus the two LOD meshes; the body carries the `UCX_` hulls as simple collision. | (no arguments; reads `/tmp/gigline/vehicle`) | `UnrealEditor-Cmd <uproject> -run=pythonscript -script=Tools/ue_vehicle_import.py -nullrhi -unattended -nopause` |
|
| `ue_vehicle_import.py` | Imports the sedan's six systems into `/Game/Hollywood/Vehicle` as separate static meshes, plus the two LOD meshes; the body carries the `UCX_` hulls as simple collision. | (no arguments; reads `/tmp/gigline/vehicle`) | `UnrealEditor-Cmd <uproject> -run=pythonscript -script=Tools/ue_vehicle_import.py -nullrhi -unattended -nopause` |
|
||||||
|
| `vehicle_from_template.py` | Converts the owner's `hot hatch.blend` into UE-ready assets: normalises the scale to a documented 4.05 m, splits systems (body/trim/glass/lights/wheels), adds a `UCX_` hull, decimates two LODs, exports FBX per system and writes a report. | `--out`, `--report` | `blender -b "Art/blender/vehicle/hot hatch.blend" --python Tools/vehicle_from_template.py -- --out /tmp/gigline/hatch` |
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
+15
-21
@@ -661,34 +661,28 @@ def build():
|
|||||||
# attached to the vehicle pawn stay invisible, so the visuals do not depend on the
|
# attached to the vehicle pawn stay invisible, so the visuals do not depend on the
|
||||||
# pawn until that is solved. A visible car that does not drive yet beats an invisible
|
# pawn until that is solved. A visible car that does not drive yet beats an invisible
|
||||||
# one that does.
|
# one that does.
|
||||||
vehicle_origin = unreal.Vector(-12950.0, -900.0, 470.0)
|
# The hot-hatch template, normalised to 4.05 m and split per the vehicle-artist
|
||||||
|
# object split. Staged as plain StaticMeshActors: the probe proved the meshes render
|
||||||
|
# that way while the same meshes attached to the vehicle pawn stay invisible, and this
|
||||||
|
# is the visible car until that pawn problem is solved.
|
||||||
|
vehicle_origin = unreal.Vector(-12950.0, -900.0, 372.0)
|
||||||
|
vehicle_rotation = unreal.Rotator(pitch=0.0, yaw=0.0, roll=0.0)
|
||||||
|
|
||||||
vehicle_class = unreal.load_class(None, "/Script/GigLine.GigLineVehicle")
|
panel_systems = (
|
||||||
if vehicle_class:
|
"SM_Vehicle_Body",
|
||||||
vehicle = spawn(vehicle_class, vehicle_origin, "Demo_Vehicle_Sedan")
|
"SM_Vehicle_Trim",
|
||||||
log(f"staged: Demo_Vehicle_Sedan ({'ok' if vehicle else 'FAILED'})")
|
"SM_Vehicle_Glass",
|
||||||
else:
|
"SM_Vehicle_Lights",
|
||||||
log("staged: Demo_Vehicle_Sedan skipped - class not found")
|
"SM_Vehicle_Wheels",
|
||||||
|
|
||||||
panels = (
|
|
||||||
("SM_Vehicle_Body", unreal.Vector(0.0, 0.0, -72.0)),
|
|
||||||
("SM_Vehicle_Glass", unreal.Vector(0.0, 0.0, -72.0)),
|
|
||||||
("SM_Vehicle_Light_Front", unreal.Vector(0.0, 0.0, -72.0)),
|
|
||||||
("SM_Vehicle_Light_Rear", unreal.Vector(0.0, 0.0, -72.0)),
|
|
||||||
("SM_Vehicle_Mirrors", unreal.Vector(0.0, 0.0, -72.0)),
|
|
||||||
("SM_Vehicle_Wheel", unreal.Vector(139.0, 81.0, -88.0)),
|
|
||||||
("SM_Vehicle_Wheel", unreal.Vector(139.0, -81.0, -88.0)),
|
|
||||||
("SM_Vehicle_Wheel", unreal.Vector(-139.0, 81.0, -88.0)),
|
|
||||||
("SM_Vehicle_Wheel", unreal.Vector(-139.0, -81.0, -88.0)),
|
|
||||||
)
|
)
|
||||||
staged_panels = 0
|
staged_panels = 0
|
||||||
for index, (mesh_name, offset) in enumerate(panels):
|
for index, mesh_name in enumerate(panel_systems):
|
||||||
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Vehicle/{mesh_name}")
|
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Vehicle/{mesh_name}")
|
||||||
if not mesh:
|
if not mesh:
|
||||||
log(f"vehicle panel {mesh_name}: MISSING")
|
log(f"vehicle panel {mesh_name}: MISSING")
|
||||||
continue
|
continue
|
||||||
actor = spawn(unreal.StaticMeshActor, vehicle_origin + offset,
|
actor = spawn(unreal.StaticMeshActor, vehicle_origin,
|
||||||
f"Demo_VehiclePanel_{index:02d}")
|
f"Demo_VehiclePanel_{index:02d}", vehicle_rotation)
|
||||||
if actor:
|
if actor:
|
||||||
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
||||||
if comp:
|
if comp:
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ try:
|
|||||||
# The sedan, staged on the carriageway at (-12950, -900). Three-quarter front
|
# The sedan, staged on the carriageway at (-12950, -900). Three-quarter front
|
||||||
# view from the road so both the body and the wheels are visible: a side-on
|
# view from the road so both the body and the wheels are visible: a side-on
|
||||||
# or rear view hides the wheel stances that the build is judged on.
|
# or rear view hides the wheel stances that the build is judged on.
|
||||||
("demo-vehicle", unreal.Vector(-12250, -1600, 620), unreal.Vector(-12950, -900, 430), 50.0),
|
("demo-vehicle", unreal.Vector(-12560, -1560, 560), unreal.Vector(-12950, -900, 420), 45.0),
|
||||||
("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620), 48.0),
|
("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620), 48.0),
|
||||||
# Street level, from the north sidewalk: the old camera sat at Y -400,
|
# Street level, from the north sidewalk: the old camera sat at Y -400,
|
||||||
# inside the facade body (which spans Y -1457..-200), so it looked at
|
# inside the facade body (which spans Y -1457..-200), so it looked at
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ import os
|
|||||||
import unreal
|
import unreal
|
||||||
|
|
||||||
DEST = "/Game/Hollywood/Vehicle"
|
DEST = "/Game/Hollywood/Vehicle"
|
||||||
SOURCE = "/tmp/gigline/vehicle"
|
SOURCE = "/tmp/gigline/hatch"
|
||||||
SYSTEMS = (
|
SYSTEMS = (
|
||||||
("body", "SM_Vehicle_Body"),
|
("body", "SM_Vehicle_Body"),
|
||||||
|
("trim", "SM_Vehicle_Trim"),
|
||||||
("glass", "SM_Vehicle_Glass"),
|
("glass", "SM_Vehicle_Glass"),
|
||||||
("light_front", "SM_Vehicle_Light_Front"),
|
("lights", "SM_Vehicle_Lights"),
|
||||||
("light_rear", "SM_Vehicle_Light_Rear"),
|
("wheels", "SM_Vehicle_Wheels"),
|
||||||
("mirrors", "SM_Vehicle_Mirrors"),
|
|
||||||
("wheel", "SM_Vehicle_Wheel"),
|
|
||||||
)
|
)
|
||||||
RESULT = "/tmp/gigline/vehicle-import.txt"
|
RESULT = "/tmp/gigline/vehicle-import.txt"
|
||||||
lines = []
|
lines = []
|
||||||
@@ -103,7 +102,8 @@ def main():
|
|||||||
log(f"{name}: imported as its own asset ({'ok' if mesh else 'MISSING'}); "
|
log(f"{name}: imported as its own asset ({'ok' if mesh else 'MISSING'}); "
|
||||||
"LOD group wiring needs an editor session")
|
"LOD group wiring needs an editor session")
|
||||||
|
|
||||||
# Skeletal vehicle for Chaos: a root bone plus one bone per wheel, everything skinned.
|
# Skeletal vehicle for Chaos is not built from this template yet: the hatch is a
|
||||||
|
# static model, and the drivable rig has to be authored around it.
|
||||||
skel_path = os.path.join(SOURCE, "vehicle_skeletal.fbx")
|
skel_path = os.path.join(SOURCE, "vehicle_skeletal.fbx")
|
||||||
if os.path.exists(skel_path):
|
if os.path.exists(skel_path):
|
||||||
task = unreal.AssetImportTask()
|
task = unreal.AssetImportTask()
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Turn the provided hot-hatch .blend into UE-ready assets.
|
||||||
|
|
||||||
|
blender -b "Art/blender/vehicle/hot hatch.blend" \
|
||||||
|
--python Tools/vehicle_from_template.py -- \
|
||||||
|
--out /tmp/gigline/hatch --report /tmp/gigline/hatch/report.json
|
||||||
|
|
||||||
|
Follows two skills:
|
||||||
|
|
||||||
|
* **vehicle-artist** — one object per system (body, wheels, glass, lights, trim,
|
||||||
|
mirrors), collision planned with the body, LODs planned before export.
|
||||||
|
* **unreal-export** — scale confirmed with a known dimension, `SM_`/`UCX_` naming,
|
||||||
|
LOD chain, and every asset measured after import.
|
||||||
|
|
||||||
|
The template is not to scale: its longest axis is 7.21 m, which no hot hatch is. The
|
||||||
|
tool therefore normalises to a documented real length (default 4.05 m) and records the
|
||||||
|
factor, rather than exporting whatever units the file happened to use. That is the
|
||||||
|
"confirm scale with known-dimension test" step, done before the export instead of after
|
||||||
|
the import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset # noqa: E402
|
||||||
|
|
||||||
|
# A three-door hot hatch: 4.05 m long, 1.75 m wide, 1.42 m tall.
|
||||||
|
TARGET_LENGTH_M = 4.05
|
||||||
|
TARGET_WIDTH_M = 1.75
|
||||||
|
TARGET_HEIGHT_M = 1.42
|
||||||
|
|
||||||
|
PARTS: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def log(message: str) -> None:
|
||||||
|
PARTS.append(message)
|
||||||
|
print(f"[hatch] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_RULES = (
|
||||||
|
# (system, material substrings that identify it, object-name substrings)
|
||||||
|
("glass", ("windows",), ("Cube.003",)),
|
||||||
|
("lights", ("light", "reflector"), ("lights and grill", "Cylinder.003", "Cylinder.004")),
|
||||||
|
("mirrors", ("mirror",), ("Cube.005",)),
|
||||||
|
("wheels", ("rubber", "brake caliper", "black shiny metal"), ("Cylinder", "Cylinder.001")),
|
||||||
|
("trim", ("plastic black", "pure black", "chrome", "black metal", "rough chrome"),
|
||||||
|
("Cube.001", "Cube.002", "Cube.004", "Cube.006", "Cube.007", "Cylinder.002")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def classify(obj):
|
||||||
|
"""Assign an object to a system by its materials first, then by its name."""
|
||||||
|
materials = [m.name.lower() for m in obj.data.materials if m]
|
||||||
|
name = obj.name.lower()
|
||||||
|
for system, materials_hint, names_hint in SYSTEM_RULES:
|
||||||
|
if any(hint in name for hint in (n.lower() for n in names_hint)):
|
||||||
|
return system
|
||||||
|
if any(any(h in material for h in materials_hint) for material in materials):
|
||||||
|
return system
|
||||||
|
return "body"
|
||||||
|
|
||||||
|
|
||||||
|
def normalise_scale(objects):
|
||||||
|
"""Scale and centre the whole car so it matches the documented dimensions."""
|
||||||
|
xs, ys, zs = [], [], []
|
||||||
|
for obj in objects:
|
||||||
|
for corner in obj.bound_box:
|
||||||
|
world = obj.matrix_world @ __import__("mathutils").Vector(corner)
|
||||||
|
xs.append(world.x)
|
||||||
|
ys.append(world.y)
|
||||||
|
zs.append(world.z)
|
||||||
|
length = max(ys) - min(ys)
|
||||||
|
width = max(xs) - min(xs)
|
||||||
|
height = max(zs) - min(zs)
|
||||||
|
log(f"template bounds: {length:.2f} x {width:.2f} x {height:.2f} m")
|
||||||
|
factor = TARGET_LENGTH_M / length if length > 0.01 else 1.0
|
||||||
|
log(f"scale factor {factor:.4f} (target length {TARGET_LENGTH_M} m, "
|
||||||
|
f"was {length:.2f} m)")
|
||||||
|
|
||||||
|
for obj in objects:
|
||||||
|
obj.scale = tuple(s * factor for s in obj.scale)
|
||||||
|
obj.location = tuple(l * factor for l in obj.location)
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
# Drop the car onto Z 0 and centre it on the origin in X/Y.
|
||||||
|
xs, ys, zs = [], [], []
|
||||||
|
for obj in objects:
|
||||||
|
for corner in obj.bound_box:
|
||||||
|
world = obj.matrix_world @ __import__("mathutils").Vector(corner)
|
||||||
|
xs.append(world.x)
|
||||||
|
ys.append(world.y)
|
||||||
|
zs.append(world.z)
|
||||||
|
offset = __import__("mathutils").Vector(
|
||||||
|
(-(min(xs) + max(xs)) / 2.0, -(min(ys) + max(ys)) / 2.0, -min(zs)))
|
||||||
|
for obj in objects:
|
||||||
|
obj.location = tuple(l + o for l, o in zip(obj.location, offset))
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
log(f"centred and grounded (offset {offset.x:.2f}, {offset.y:.2f}, {offset.z:.2f} m)")
|
||||||
|
|
||||||
|
|
||||||
|
def join_system(objects, name):
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
for obj in objects:
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = objects[0]
|
||||||
|
if len(objects) > 1:
|
||||||
|
bpy.ops.object.join()
|
||||||
|
joined = bpy.context.active_object
|
||||||
|
joined.name = name
|
||||||
|
return joined
|
||||||
|
|
||||||
|
|
||||||
|
def add_collision(body):
|
||||||
|
"""One convex box per major volume, `UCX_`-prefixed for Unreal."""
|
||||||
|
hulls = []
|
||||||
|
xs, ys, zs = [], [], []
|
||||||
|
for corner in body.bound_box:
|
||||||
|
world = body.matrix_world @ __import__("mathutils").Vector(corner)
|
||||||
|
xs.append(world.x)
|
||||||
|
ys.append(world.y)
|
||||||
|
zs.append(world.z)
|
||||||
|
length, width, height = max(ys) - min(ys), max(xs) - min(xs), max(zs) - min(zs)
|
||||||
|
centre = ((min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0,
|
||||||
|
min(zs) + height * 0.5)
|
||||||
|
bpy.ops.mesh.primitive_cube_add(size=1.0, location=centre)
|
||||||
|
hull = bpy.context.active_object
|
||||||
|
hull.name = "UCX_SM_Vehicle_Body_01"
|
||||||
|
hull.scale = (width * 0.96, length * 0.98, height * 0.62)
|
||||||
|
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||||
|
hull.hide_render = True
|
||||||
|
hulls.append(hull)
|
||||||
|
log(f"collision: 1 hull {width*0.96:.2f} x {length*0.98:.2f} x {height*0.62:.2f} m")
|
||||||
|
return hulls
|
||||||
|
|
||||||
|
|
||||||
|
def decimate_copy(source, name, ratio):
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
source.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = source
|
||||||
|
bpy.ops.object.duplicate()
|
||||||
|
copy = bpy.context.active_object
|
||||||
|
copy.name = name
|
||||||
|
modifier = copy.modifiers.new(name="LOD", type="DECIMATE")
|
||||||
|
modifier.ratio = ratio
|
||||||
|
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||||
|
return copy
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--out", default="/tmp/gigline/hatch")
|
||||||
|
ap.add_argument("--report", default="/tmp/gigline/hatch/report.json")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
meshes = [o for o in bpy.data.objects if o.type == "MESH"]
|
||||||
|
if not meshes:
|
||||||
|
raise SystemExit("no meshes in the template")
|
||||||
|
log(f"template: {len(meshes)} mesh objects, "
|
||||||
|
f"{sum(len(o.data.vertices) for o in meshes)} vertices")
|
||||||
|
|
||||||
|
normalise_scale(meshes)
|
||||||
|
|
||||||
|
systems = {}
|
||||||
|
for obj in meshes:
|
||||||
|
systems.setdefault(classify(obj), []).append(obj)
|
||||||
|
log("systems: " + ", ".join(f"{k}={len(v)} objects" for k, v in systems.items()))
|
||||||
|
|
||||||
|
os.makedirs(args.out, exist_ok=True)
|
||||||
|
exported = {}
|
||||||
|
joined = {}
|
||||||
|
for system, objects in systems.items():
|
||||||
|
name = {
|
||||||
|
"body": "SM_Vehicle_Body",
|
||||||
|
"glass": "SM_Vehicle_Glass",
|
||||||
|
"lights": "SM_Vehicle_Lights",
|
||||||
|
"mirrors": "SM_Vehicle_Mirrors",
|
||||||
|
"wheels": "SM_Vehicle_Wheels",
|
||||||
|
"trim": "SM_Vehicle_Trim",
|
||||||
|
}[system]
|
||||||
|
obj = join_system(objects, name)
|
||||||
|
joined[system] = obj
|
||||||
|
path = os.path.join(args.out, f"{system}.fbx")
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
fbx_export_preset.export_static(path, selection=True)
|
||||||
|
exported[system] = path
|
||||||
|
tris = sum(len(p.vertices) - 2 for p in obj.data.polygons)
|
||||||
|
log(f"{name}: {tris} triangles -> {path}")
|
||||||
|
|
||||||
|
hulls = add_collision(joined["body"])
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
joined["body"].select_set(True)
|
||||||
|
for hull in hulls:
|
||||||
|
hull.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = joined["body"]
|
||||||
|
fbx_export_preset.export_static(os.path.join(args.out, "body.fbx"), selection=True)
|
||||||
|
log("body re-exported with its UCX hull")
|
||||||
|
|
||||||
|
lods = {}
|
||||||
|
for index, ratio in ((1, 0.35), (2, 0.12)):
|
||||||
|
lod = decimate_copy(joined["body"], f"SM_Vehicle_Body_LOD{index}", ratio)
|
||||||
|
path = os.path.join(args.out, f"body_lod{index}.fbx")
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
lod.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = lod
|
||||||
|
fbx_export_preset.export_static(path, selection=True)
|
||||||
|
tris = sum(len(p.vertices) - 2 for p in lod.data.polygons)
|
||||||
|
lods[f"body_lod{index}"] = {"path": path, "triangles": tris}
|
||||||
|
log(f"SM_Vehicle_Body_LOD{index}: {tris} triangles")
|
||||||
|
bpy.data.objects.remove(lod, do_unlink=True)
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"template": bpy.data.filepath,
|
||||||
|
"target_dimensions_m": {"length": TARGET_LENGTH_M, "width": TARGET_WIDTH_M,
|
||||||
|
"height": TARGET_HEIGHT_M},
|
||||||
|
"systems": {system: {"object": joined[system].name,
|
||||||
|
"triangles": sum(len(p.vertices) - 2
|
||||||
|
for p in joined[system].data.polygons),
|
||||||
|
"fbx": exported.get(system)}
|
||||||
|
for system in joined},
|
||||||
|
"lods": lods,
|
||||||
|
"collision": [h.name for h in hulls],
|
||||||
|
"log": PARTS,
|
||||||
|
}
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(args.report)), exist_ok=True)
|
||||||
|
with open(args.report, "w") as handle:
|
||||||
|
json.dump(report, handle, indent=2)
|
||||||
|
print(f"[hatch] report -> {args.report}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in new issue
Block a user