unitytk 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unitytk-0.2.1/unitytk.egg-info → unitytk-0.2.2}/PKG-INFO +1 -1
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_asset_delivery.py +26 -8
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_audio_events_integration.py +167 -16
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_embedded_dependencies_integration.py +183 -1
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/__init__.py +1 -1
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/asset_delivery.py +1 -2
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs +5 -2
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/AudioEvent/AudioEventController.cs +48 -19
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/README.md +1 -1
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/UnitytkSettings.cs +60 -8
- {unitytk-0.2.1 → unitytk-0.2.2/unitytk.egg-info}/PKG-INFO +1 -1
- {unitytk-0.2.1 → unitytk-0.2.2}/LICENSE +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/MANIFEST.in +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/docs/README.md +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/pyproject.toml +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/setup.cfg +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_articulated_rig_runtime.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_base_test.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_blender_render_effects_e2e.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_blender_shadow_plane_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_blender_shot_metadata_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_bool_to_fade_curve.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_clip_name_contract_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_controller_runtime_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_deploy_templates.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_emissive_groups_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_fade_window_preservation.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_keyed_visibility.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_launcher.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_lightmap_metadata_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_maya_surface_materials_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_multi_section_audio_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_render_effects_e2e.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_render_opacity_controller.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_render_opacity_e2e.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_render_opacity_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_run_tests.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_scene_builder.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_shadow_plane_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_shadow_plane_runtime_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_shared_material_standalone.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_shot_metadata_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_shots_audio_sidebyside_integration.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_standalone_enduser.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/test/test_temp_projects.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/launcher.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/scene_builder.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/template_deployer.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/AudioEvent/AudioEventController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/AudioEvent.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroups.hlsl.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/M3trik.Unitytk.asmdef.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/MayaSurface.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/README.md.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/RenderEffects/RenderEffectsController.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/RenderEffects.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlane.shader +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlane.shader.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane/ShadowPlaneHorizon.hlsl.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShadowPlane.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShotMetadata/ShotMetadataController.cs +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShotMetadata.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/UnitytkSettings.cs.meta +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/__init__.py +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk.egg-info/SOURCES.txt +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk.egg-info/requires.txt +0 -0
- {unitytk-0.2.1 → unitytk-0.2.2}/unitytk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# !/usr/bin/python
|
|
2
2
|
# coding=utf-8
|
|
3
3
|
"""Tests for unitytk.CopyToAssetsDeliverer -- copy-to-Assets hand-off Strategy (no DCC)."""
|
|
4
|
+
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import struct
|
|
@@ -20,6 +21,7 @@ class _Logger:
|
|
|
20
21
|
self.errors = []
|
|
21
22
|
self.warnings = []
|
|
22
23
|
self.infos = []
|
|
24
|
+
self.debugs = []
|
|
23
25
|
|
|
24
26
|
def error(self, m, *args):
|
|
25
27
|
self.errors.append(m % args if args else m)
|
|
@@ -30,6 +32,9 @@ class _Logger:
|
|
|
30
32
|
def info(self, m, *args):
|
|
31
33
|
self.infos.append(m % args if args else m)
|
|
32
34
|
|
|
35
|
+
def debug(self, m, *args):
|
|
36
|
+
self.debugs.append(m % args if args else m)
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
def _carrier_fbx(path: str, channels) -> str:
|
|
35
40
|
"""A binary FBX (7700 layout) whose ``data_export`` carrier holds
|
|
@@ -108,7 +113,9 @@ def _payload(fbx, default_name="Export"):
|
|
|
108
113
|
|
|
109
114
|
class SanitizeTest(unittest.TestCase):
|
|
110
115
|
def test_strips_invalid_and_paths(self):
|
|
111
|
-
self.assertEqual(
|
|
116
|
+
self.assertEqual(
|
|
117
|
+
CopyToAssetsDeliverer.sanitize_asset_name("my/model:v2"), "my_model_v2"
|
|
118
|
+
)
|
|
112
119
|
|
|
113
120
|
def test_collapses_and_trims(self):
|
|
114
121
|
self.assertEqual(CopyToAssetsDeliverer.sanitize_asset_name(" a b "), "a b")
|
|
@@ -193,7 +200,9 @@ class DeliveryTest(unittest.TestCase):
|
|
|
193
200
|
)
|
|
194
201
|
|
|
195
202
|
def test_explicit_asset_name_overrides_and_sanitizes(self):
|
|
196
|
-
_bridge, result = self._deliver(
|
|
203
|
+
_bridge, result = self._deliver(
|
|
204
|
+
{"ASSET_NAME": "Hero/Mesh:01"}, default_name="Cube"
|
|
205
|
+
)
|
|
197
206
|
self.assertEqual(os.path.basename(result["asset"]), "Hero_Mesh_01.fbx")
|
|
198
207
|
|
|
199
208
|
def test_launch_mode_open_invokes_windowed_launch(self):
|
|
@@ -282,6 +291,7 @@ class DeliveryTest(unittest.TestCase):
|
|
|
282
291
|
orig = ad.CopyToAssetsDeliverer._launch_editor
|
|
283
292
|
# Patch UnityLauncher inside the real hook (deferred import target).
|
|
284
293
|
import unitytk.launcher as launcher_mod
|
|
294
|
+
|
|
285
295
|
real = launcher_mod.UnityLauncher
|
|
286
296
|
launcher_mod.UnityLauncher = _FakeLauncher
|
|
287
297
|
try:
|
|
@@ -344,15 +354,14 @@ class DeliveryTest(unittest.TestCase):
|
|
|
344
354
|
)
|
|
345
355
|
# Genuinely under Assets/ (no drive escape).
|
|
346
356
|
assets = os.path.realpath(os.path.join(self.project, "Assets"))
|
|
347
|
-
self.assertTrue(
|
|
348
|
-
os.path.realpath(dest).startswith(assets), f"raw={raw!r}"
|
|
349
|
-
)
|
|
357
|
+
self.assertTrue(os.path.realpath(dest).startswith(assets), f"raw={raw!r}")
|
|
350
358
|
|
|
351
359
|
def test_explicit_subdir_reducing_to_nothing_drops_into_assets_root(self):
|
|
352
360
|
for raw in (".", "/", "../.."):
|
|
353
361
|
_bridge, result = self._deliver({"ASSETS_SUBDIR": raw}, default_name="Cube")
|
|
354
362
|
self.assertEqual(
|
|
355
|
-
result["asset"],
|
|
363
|
+
result["asset"],
|
|
364
|
+
os.path.join(self.project, "Assets", "Cube.fbx"),
|
|
356
365
|
f"raw={raw!r}",
|
|
357
366
|
)
|
|
358
367
|
self.assertEqual(result["subdir"], "")
|
|
@@ -388,7 +397,11 @@ class SelfContainedDeliveryTest(unittest.TestCase):
|
|
|
388
397
|
"lightmap_metadata": {
|
|
389
398
|
"version": 1,
|
|
390
399
|
"objects": [{"name": "Crate", "map": "Crate_Lightmap.exr", "uvIndex": 1}],
|
|
391
|
-
"probe": {
|
|
400
|
+
"probe": {
|
|
401
|
+
"map": "Room_Probe.exr",
|
|
402
|
+
"position": [0, 0, 0],
|
|
403
|
+
"unit_scale": 0.01,
|
|
404
|
+
},
|
|
392
405
|
},
|
|
393
406
|
"shadow_metadata": {
|
|
394
407
|
"version": 2,
|
|
@@ -411,7 +424,12 @@ class SelfContainedDeliveryTest(unittest.TestCase):
|
|
|
411
424
|
self.maps = os.path.join(self.tmp, "sourceimages")
|
|
412
425
|
os.makedirs(self.maps)
|
|
413
426
|
self.bytes = {}
|
|
414
|
-
for name in (
|
|
427
|
+
for name in (
|
|
428
|
+
"Crate_Lightmap.exr",
|
|
429
|
+
"Room_Probe.exr",
|
|
430
|
+
"Crate_shadow.png",
|
|
431
|
+
"Crate_horizon.png",
|
|
432
|
+
):
|
|
415
433
|
self.bytes[name] = name.encode() * 8
|
|
416
434
|
with open(os.path.join(self.maps, name), "wb") as fh:
|
|
417
435
|
fh.write(self.bytes[name])
|
|
@@ -24,6 +24,7 @@ Requires:
|
|
|
24
24
|
- Maya 2025 installed (mayapy.exe)
|
|
25
25
|
- Unity Hub with at least one editor installed
|
|
26
26
|
"""
|
|
27
|
+
|
|
27
28
|
import os
|
|
28
29
|
import json
|
|
29
30
|
import shutil
|
|
@@ -34,7 +35,15 @@ import logging
|
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
35
36
|
|
|
36
37
|
import base_test # noqa: F401 — sys.path bootstrap for the sibling repos
|
|
37
|
-
from base_test import
|
|
38
|
+
from base_test import (
|
|
39
|
+
BlenderEnv,
|
|
40
|
+
CarrierFbx,
|
|
41
|
+
EnvSkip,
|
|
42
|
+
MayaEnv,
|
|
43
|
+
TempProjects,
|
|
44
|
+
UnityEnv,
|
|
45
|
+
UserPropsProbe,
|
|
46
|
+
)
|
|
38
47
|
|
|
39
48
|
scripts_dir = base_test.SCRIPTS_ROOT
|
|
40
49
|
|
|
@@ -471,9 +480,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
471
480
|
events,
|
|
472
481
|
{("", 9, "footstep"), ("", 29, "jump"), ("", 44, "land")},
|
|
473
482
|
)
|
|
474
|
-
self.assertEqual(
|
|
475
|
-
sorted(r["tracks"]), ["footstep", "jump", "land"]
|
|
476
|
-
)
|
|
483
|
+
self.assertEqual(sorted(r["tracks"]), ["footstep", "jump", "land"])
|
|
477
484
|
|
|
478
485
|
# ------------------------------------------------------------------
|
|
479
486
|
# Step 2: Unity import and readback
|
|
@@ -552,17 +559,26 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
552
559
|
full_log = f.read()
|
|
553
560
|
# Surface AudioEvent / error / warning lines for analysis
|
|
554
561
|
relevant = [
|
|
555
|
-
ln
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
562
|
+
ln
|
|
563
|
+
for ln in full_log.splitlines()
|
|
564
|
+
if any(
|
|
565
|
+
s in ln
|
|
566
|
+
for s in (
|
|
567
|
+
"AudioEvent",
|
|
568
|
+
"ERROR",
|
|
569
|
+
"Error",
|
|
570
|
+
"Warning",
|
|
571
|
+
"NullReferenceException",
|
|
572
|
+
"Failed",
|
|
573
|
+
)
|
|
574
|
+
)
|
|
560
575
|
]
|
|
561
576
|
self.__class__.unity_log_relevant = relevant
|
|
562
577
|
if relevant:
|
|
563
578
|
logger.info(
|
|
564
579
|
"Unity log relevant lines (%d):\n %s",
|
|
565
|
-
len(relevant),
|
|
580
|
+
len(relevant),
|
|
581
|
+
"\n ".join(relevant[-40:]),
|
|
566
582
|
)
|
|
567
583
|
else:
|
|
568
584
|
logger.info("Unity log: no AudioEvent/Warning/Error lines.")
|
|
@@ -571,8 +587,7 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
571
587
|
with open(results_path) as f:
|
|
572
588
|
self.__class__.unity_results = json.load(f)
|
|
573
589
|
|
|
574
|
-
postproc_path = os.path.join(
|
|
575
|
-
self.temp_dir, UserPropsProbe.RESULTS_FILENAME)
|
|
590
|
+
postproc_path = os.path.join(self.temp_dir, UserPropsProbe.RESULTS_FILENAME)
|
|
576
591
|
if os.path.exists(postproc_path):
|
|
577
592
|
with open(postproc_path) as f:
|
|
578
593
|
self.__class__.postproc = json.load(f)
|
|
@@ -608,7 +623,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
608
623
|
f"in postprocessor results: {list(postproc)}",
|
|
609
624
|
)
|
|
610
625
|
self.assertIn(
|
|
611
|
-
"audio_manifest",
|
|
626
|
+
"audio_manifest",
|
|
627
|
+
props,
|
|
612
628
|
f"audio_manifest user-property missing on {name}; saw: {list(props)}",
|
|
613
629
|
)
|
|
614
630
|
manifest = props["audio_manifest"]
|
|
@@ -655,7 +671,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
655
671
|
|
|
656
672
|
# (d) AnimationEvents injected on the imported clip
|
|
657
673
|
self.assertGreaterEqual(
|
|
658
|
-
unity.get("clip_count", 0),
|
|
674
|
+
unity.get("clip_count", 0),
|
|
675
|
+
1,
|
|
659
676
|
"Expected at least one AnimationClip in the imported FBX",
|
|
660
677
|
)
|
|
661
678
|
events = unity.get("animation_events", [])
|
|
@@ -676,8 +693,8 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
676
693
|
# In Unity-frame terms the events are at clip-relative frames 9,
|
|
677
694
|
# 29, 44 — which is what AnimationEvent.time * frameRate yields.
|
|
678
695
|
self.assertEqual(events_by_name.get("footstep"), 9)
|
|
679
|
-
self.assertEqual(events_by_name.get("jump"),
|
|
680
|
-
self.assertEqual(events_by_name.get("land"),
|
|
696
|
+
self.assertEqual(events_by_name.get("jump"), 29)
|
|
697
|
+
self.assertEqual(events_by_name.get("land"), 44)
|
|
681
698
|
|
|
682
699
|
# (e) Reimport idempotency. Two probes injected before the
|
|
683
700
|
# reimport tell us about both correctness and Unity behavior:
|
|
@@ -721,6 +738,140 @@ class TestAudioEventsIntegration(unittest.TestCase):
|
|
|
721
738
|
)
|
|
722
739
|
|
|
723
740
|
|
|
741
|
+
# ======================================================================
|
|
742
|
+
# Carrier precedence: one audio_manifest per model, the scene's own first
|
|
743
|
+
# ======================================================================
|
|
744
|
+
|
|
745
|
+
CARRIER_VERIFIER_CS = r"""
|
|
746
|
+
using UnityEngine;
|
|
747
|
+
using UnityEditor;
|
|
748
|
+
using System;
|
|
749
|
+
using System.IO;
|
|
750
|
+
using System.Linq;
|
|
751
|
+
using System.Collections.Generic;
|
|
752
|
+
|
|
753
|
+
public class AudioCarrierVerifier
|
|
754
|
+
{
|
|
755
|
+
static string Q(string s) { return "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }
|
|
756
|
+
|
|
757
|
+
public static void Verify()
|
|
758
|
+
{
|
|
759
|
+
var o = new List<string>();
|
|
760
|
+
try
|
|
761
|
+
{
|
|
762
|
+
// Again, now the clips are in the database: the startup import
|
|
763
|
+
// may have met the model before the audio.
|
|
764
|
+
AssetDatabase.ImportAsset("__FBX__",
|
|
765
|
+
ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
|
766
|
+
AssetDatabase.Refresh();
|
|
767
|
+
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("__FBX__");
|
|
768
|
+
o.Add("\"prefab\":" + (prefab != null ? "true" : "false"));
|
|
769
|
+
var nodes = prefab != null
|
|
770
|
+
? prefab.GetComponentsInChildren<Transform>(true).Select(t => Q(t.name))
|
|
771
|
+
: Enumerable.Empty<string>();
|
|
772
|
+
o.Add("\"nodes\":[" + string.Join(",", nodes) + "]");
|
|
773
|
+
var ctrl = prefab != null ? prefab.GetComponent<AudioEventController>() : null;
|
|
774
|
+
o.Add("\"controller\":" + (ctrl != null ? "true" : "false"));
|
|
775
|
+
var clips = ctrl != null ? ctrl.clips : new List<AudioClip>();
|
|
776
|
+
o.Add("\"clips\":[" + string.Join(",", clips.Select(c => Q(c != null ? c.name : ""))) + "]");
|
|
777
|
+
}
|
|
778
|
+
catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
|
|
779
|
+
File.WriteAllText(@"__RESULTS__", "{" + string.Join(",\n", o) + "}");
|
|
780
|
+
EditorApplication.Exit(0);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class TestAudioCarrierPrecedence(unittest.TestCase):
|
|
787
|
+
"""An FBX carrying ``audio_manifest`` on several carriers plays the
|
|
788
|
+
scene's own only.
|
|
789
|
+
|
|
790
|
+
An assembly ships a stray ``data_export1`` (and a module's
|
|
791
|
+
``NS:data_export``) beside the scene's own, each holding the events as
|
|
792
|
+
last exported alone -- measured on an assembly, 2026-10-13. The scene's
|
|
793
|
+
own record is re-derived from the whole merged scene, so every reader
|
|
794
|
+
takes ONE carrier per channel, the scene's own first: the other
|
|
795
|
+
importers (``CarrierImport.Collect``), the GLB applier, and pythontk's
|
|
796
|
+
``FbxMedia.audio_events``. The audio importer concatenated every
|
|
797
|
+
carrier's events instead, so the stray's stale event was bound and fired.
|
|
798
|
+
|
|
799
|
+
The fixture is a real FBX with both carriers (``base_test.CarrierFbx``,
|
|
800
|
+
written by a fresh Blender); pythontk's ``FbxMedia.audio_events`` pins the
|
|
801
|
+
same rule on the same shape (``test_fbx_media``).
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
FBX = "Assets/Delivered/assembly.fbx"
|
|
805
|
+
|
|
806
|
+
@classmethod
|
|
807
|
+
def setUpClass(cls):
|
|
808
|
+
cls.temp_dir = TempProjects.allocate("audio_carrier_precedence")
|
|
809
|
+
|
|
810
|
+
@BlenderEnv.skip_unless_available()
|
|
811
|
+
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
812
|
+
def test_the_scenes_own_carrier_wins(self):
|
|
813
|
+
own = {"version": 2, "events": [{"clip": "", "frame": 0, "name": "footstep"}]}
|
|
814
|
+
stray = {"version": 2, "events": [{"clip": "", "frame": 3, "name": "door"}]}
|
|
815
|
+
fixture = CarrierFbx.build(
|
|
816
|
+
os.path.join(self.temp_dir, "assembly.fbx"),
|
|
817
|
+
{
|
|
818
|
+
"data_export": {"audio_manifest": own},
|
|
819
|
+
"data_export1": {"audio_manifest": stray},
|
|
820
|
+
},
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
launcher = UnityLauncher()
|
|
824
|
+
project = os.path.join(self.temp_dir, "UnityProject")
|
|
825
|
+
if not launcher.create_project(project, batch_mode=True):
|
|
826
|
+
self.skipTest(UnityEnv.project_creation_reason())
|
|
827
|
+
delivered = os.path.join(project, "Assets", "Delivered")
|
|
828
|
+
os.makedirs(delivered, exist_ok=True)
|
|
829
|
+
shutil.copy2(fixture, os.path.join(delivered, "assembly.fbx"))
|
|
830
|
+
# Both tracks resolve, so only the carrier rule decides what binds.
|
|
831
|
+
for name in ("footstep", "door"):
|
|
832
|
+
CarrierFbx.wav(
|
|
833
|
+
os.path.join(project, "Assets", "Audio", f"{name}.wav"), name.encode()
|
|
834
|
+
)
|
|
835
|
+
TemplateDeployer.deploy_package(project, components=["audio_event"])
|
|
836
|
+
results = os.path.join(self.temp_dir, "unity_results.json")
|
|
837
|
+
editor_dir = os.path.join(project, "Assets", "Editor")
|
|
838
|
+
os.makedirs(editor_dir, exist_ok=True)
|
|
839
|
+
with open(
|
|
840
|
+
os.path.join(editor_dir, "AudioCarrierVerifier.cs"), "w", encoding="utf-8"
|
|
841
|
+
) as fh:
|
|
842
|
+
fh.write(
|
|
843
|
+
CARRIER_VERIFIER_CS.replace("__FBX__", self.FBX).replace(
|
|
844
|
+
"__RESULTS__", results
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
log = os.path.join(self.temp_dir, "unity_log.txt")
|
|
848
|
+
launcher.project_path = project
|
|
849
|
+
proc = launcher.launch_editor(
|
|
850
|
+
batch_mode=True,
|
|
851
|
+
execute_method="AudioCarrierVerifier.Verify",
|
|
852
|
+
log_file=log,
|
|
853
|
+
extra_args=["-quit"],
|
|
854
|
+
detached=False,
|
|
855
|
+
)
|
|
856
|
+
returncode = None
|
|
857
|
+
if proc:
|
|
858
|
+
try:
|
|
859
|
+
returncode = proc.wait(timeout=900)
|
|
860
|
+
except subprocess.TimeoutExpired:
|
|
861
|
+
proc.kill()
|
|
862
|
+
raise TimeoutError("Unity timed out")
|
|
863
|
+
UnityEnv.require_results(self, results, returncode, log)
|
|
864
|
+
with open(results) as fh:
|
|
865
|
+
unity = json.load(fh)
|
|
866
|
+
logger.info(f"Unity results:\n{json.dumps(unity, indent=2)}")
|
|
867
|
+
self.assertNotIn("error", unity, unity.get("error"))
|
|
868
|
+
self.assertTrue(unity["prefab"], unity)
|
|
869
|
+
# Premise: both carriers reached the import.
|
|
870
|
+
self.assertTrue({"data_export", "data_export1"} <= set(unity["nodes"]), unity)
|
|
871
|
+
self.assertTrue(unity["controller"], unity)
|
|
872
|
+
self.assertEqual(unity["clips"], ["footstep"], unity)
|
|
873
|
+
|
|
874
|
+
|
|
724
875
|
if __name__ == "__main__":
|
|
725
876
|
logging.basicConfig(level=logging.INFO)
|
|
726
877
|
unittest.main()
|
|
@@ -38,7 +38,7 @@ import subprocess
|
|
|
38
38
|
import unittest
|
|
39
39
|
|
|
40
40
|
import base_test # noqa: F401 -- sys.path bootstrap for the sibling repos
|
|
41
|
-
from base_test import EnvSkip, MayaEnv, TempProjects, UnityEnv
|
|
41
|
+
from base_test import BlenderEnv, CarrierFbx, EnvSkip, MayaEnv, TempProjects, UnityEnv
|
|
42
42
|
|
|
43
43
|
from unitytk import TemplateDeployer, UnityFinder, UnityLauncher
|
|
44
44
|
|
|
@@ -456,6 +456,188 @@ class TestEmbeddedDependenciesIntegration(unittest.TestCase):
|
|
|
456
456
|
self.assertTrue(unity["reexport_still_lightmap"], unity)
|
|
457
457
|
|
|
458
458
|
|
|
459
|
+
# ======================================================================
|
|
460
|
+
# When an import asks for the FBX's embedded files to be extracted
|
|
461
|
+
# ======================================================================
|
|
462
|
+
|
|
463
|
+
REQUEST_VERIFIER_CS = r"""
|
|
464
|
+
using UnityEngine;
|
|
465
|
+
using UnityEditor;
|
|
466
|
+
using System;
|
|
467
|
+
using System.IO;
|
|
468
|
+
using System.Linq;
|
|
469
|
+
using System.Collections.Generic;
|
|
470
|
+
|
|
471
|
+
public class EmbeddedRequestVerifier
|
|
472
|
+
{
|
|
473
|
+
static string Q(string s) { return "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }
|
|
474
|
+
static string B(bool b) { return b ? "true" : "false"; }
|
|
475
|
+
|
|
476
|
+
static void Finish(List<string> o)
|
|
477
|
+
{
|
|
478
|
+
File.WriteAllText(@"__RESULTS__", "{" + string.Join(",\n", o) + "}");
|
|
479
|
+
EditorApplication.Exit(0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Session 1: the audio is in the database; the models arrive now.
|
|
483
|
+
public static void First()
|
|
484
|
+
{
|
|
485
|
+
var o = new List<string>();
|
|
486
|
+
try
|
|
487
|
+
{
|
|
488
|
+
foreach (var pair in new[] {
|
|
489
|
+
new[] { @"__RESOLVED_SRC__", "Assets/Resolved/resolved.fbx" },
|
|
490
|
+
new[] { @"__GHOSTED_SRC__", "Assets/Ghosted/ghosted.fbx" } })
|
|
491
|
+
{
|
|
492
|
+
Directory.CreateDirectory(Path.GetDirectoryName(pair[1]));
|
|
493
|
+
File.Copy(pair[0], pair[1], true);
|
|
494
|
+
}
|
|
495
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
496
|
+
o.Add("\"resolved_beside\":" + B(File.Exists("Assets/Resolved/footstep.wav")));
|
|
497
|
+
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Resolved/resolved.fbx");
|
|
498
|
+
var ctrl = prefab != null ? prefab.GetComponent<AudioEventController>() : null;
|
|
499
|
+
var clip = ctrl != null && ctrl.clips.Count > 0 ? ctrl.clips[0] : null;
|
|
500
|
+
o.Add("\"resolved_clip\":" + Q(clip != null ? AssetDatabase.GetAssetPath(clip) : ""));
|
|
501
|
+
o.Add("\"hit_beside\":" + B(File.Exists("Assets/Ghosted/hit.wav")));
|
|
502
|
+
}
|
|
503
|
+
catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
|
|
504
|
+
Finish(o);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Session 2: hit.wav was touched up beside the model; the model reimports.
|
|
508
|
+
public static void Second()
|
|
509
|
+
{
|
|
510
|
+
var o = new List<string>();
|
|
511
|
+
try
|
|
512
|
+
{
|
|
513
|
+
AssetDatabase.ImportAsset("Assets/Ghosted/ghosted.fbx",
|
|
514
|
+
ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
|
|
515
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
516
|
+
o.Add("\"touched_kept\":" + B(File.ReadAllBytes("Assets/Ghosted/hit.wav")
|
|
517
|
+
.SequenceEqual(File.ReadAllBytes(@"__TOUCHED__"))));
|
|
518
|
+
}
|
|
519
|
+
catch (Exception e) { o.Add("\"error\":" + Q(e.GetType().Name + ": " + e.Message)); }
|
|
520
|
+
Finish(o);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class TestEmbeddedRequestPolicy(unittest.TestCase):
|
|
527
|
+
"""When an import asks for the FBX's embedded files to be extracted
|
|
528
|
+
beside it (``CarrierImport.RequestEmbeddedFiles``).
|
|
529
|
+
|
|
530
|
+
Two ways it overwrote what the user had:
|
|
531
|
+
|
|
532
|
+
- a name its importer finds ELSEWHERE (the audio lookup searches
|
|
533
|
+
``Assets/Audio``) counted as missing, because only the model's own folder
|
|
534
|
+
was looked in -- so the whole FBX was extracted and the clip rebound to
|
|
535
|
+
the copy beside the model;
|
|
536
|
+
- the memo of what was already asked lived for one editor session, so a
|
|
537
|
+
model naming a file it does not embed asked again on every session's
|
|
538
|
+
reimport, and each extraction overwrote the files beside it -- a
|
|
539
|
+
touched-up clip included.
|
|
540
|
+
|
|
541
|
+
The fixture is two real FBXs (``base_test.CarrierFbx``) with their audio
|
|
542
|
+
embedded as the DCC exporters do (``ptk.FbxMedia.embed``).
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
@classmethod
|
|
546
|
+
def setUpClass(cls):
|
|
547
|
+
cls.temp_dir = TempProjects.allocate("embedded_request_policy")
|
|
548
|
+
|
|
549
|
+
def _launch(self, launcher, method: str, results: str) -> dict:
|
|
550
|
+
log = os.path.join(self.temp_dir, f"unity_{method}.txt")
|
|
551
|
+
proc = launcher.launch_editor(
|
|
552
|
+
batch_mode=True,
|
|
553
|
+
execute_method=f"EmbeddedRequestVerifier.{method}",
|
|
554
|
+
log_file=log,
|
|
555
|
+
extra_args=["-quit"],
|
|
556
|
+
detached=False,
|
|
557
|
+
)
|
|
558
|
+
returncode = None
|
|
559
|
+
if proc:
|
|
560
|
+
try:
|
|
561
|
+
returncode = proc.wait(timeout=900)
|
|
562
|
+
except subprocess.TimeoutExpired:
|
|
563
|
+
proc.kill()
|
|
564
|
+
raise TimeoutError("Unity timed out")
|
|
565
|
+
UnityEnv.require_results(self, results, returncode, log)
|
|
566
|
+
with open(results) as fh:
|
|
567
|
+
unity = json.load(fh)
|
|
568
|
+
logger.info(f"Unity {method}:\n{json.dumps(unity, indent=2)}")
|
|
569
|
+
self.assertNotIn("error", unity, unity.get("error"))
|
|
570
|
+
return unity
|
|
571
|
+
|
|
572
|
+
@BlenderEnv.skip_unless_available()
|
|
573
|
+
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
574
|
+
def test_found_elsewhere_or_asked_before_is_not_extracted_again(self):
|
|
575
|
+
import pythontk as ptk
|
|
576
|
+
|
|
577
|
+
def manifest(*names):
|
|
578
|
+
events = [{"clip": "", "frame": 0, "name": n} for n in names]
|
|
579
|
+
return {"data_export": {"audio_manifest": {"version": 2, "events": events}}}
|
|
580
|
+
|
|
581
|
+
staged = os.path.join(self.temp_dir, "staged")
|
|
582
|
+
os.makedirs(staged)
|
|
583
|
+
sources = {}
|
|
584
|
+
for stem, names, embed in (
|
|
585
|
+
("resolved", ("footstep",), "footstep"),
|
|
586
|
+
# "ghost" is embedded nowhere and resolves nowhere: always missing.
|
|
587
|
+
("ghosted", ("hit", "ghost"), "hit"),
|
|
588
|
+
):
|
|
589
|
+
bare = CarrierFbx.build(
|
|
590
|
+
os.path.join(self.temp_dir, f"{stem}_bare.fbx"), manifest(*names)
|
|
591
|
+
)
|
|
592
|
+
clip = CarrierFbx.wav(
|
|
593
|
+
os.path.join(self.temp_dir, "sound", f"{embed}.wav"), b"embedded"
|
|
594
|
+
)
|
|
595
|
+
sources[stem] = os.path.join(staged, f"{stem}.fbx")
|
|
596
|
+
ptk.FbxMedia.embed(bare, sources[stem], files=[(f"{embed}.wav", clip)])
|
|
597
|
+
|
|
598
|
+
launcher = UnityLauncher()
|
|
599
|
+
project = os.path.join(self.temp_dir, "UnityProject")
|
|
600
|
+
if not launcher.create_project(project, batch_mode=True):
|
|
601
|
+
self.skipTest(UnityEnv.project_creation_reason())
|
|
602
|
+
CarrierFbx.wav(
|
|
603
|
+
os.path.join(project, "Assets", "Audio", "footstep.wav"), b"library"
|
|
604
|
+
)
|
|
605
|
+
touched = CarrierFbx.wav(os.path.join(self.temp_dir, "touched.wav"), b"touched")
|
|
606
|
+
TemplateDeployer.deploy_package(project, components=["audio_event"])
|
|
607
|
+
results = os.path.join(self.temp_dir, "unity_results.json")
|
|
608
|
+
editor_dir = os.path.join(project, "Assets", "Editor")
|
|
609
|
+
os.makedirs(editor_dir, exist_ok=True)
|
|
610
|
+
with open(
|
|
611
|
+
os.path.join(editor_dir, "EmbeddedRequestVerifier.cs"),
|
|
612
|
+
"w",
|
|
613
|
+
encoding="utf-8",
|
|
614
|
+
) as fh:
|
|
615
|
+
fh.write(
|
|
616
|
+
REQUEST_VERIFIER_CS.replace("__RESULTS__", results)
|
|
617
|
+
.replace("__RESOLVED_SRC__", sources["resolved"])
|
|
618
|
+
.replace("__GHOSTED_SRC__", sources["ghosted"])
|
|
619
|
+
.replace("__TOUCHED__", touched)
|
|
620
|
+
)
|
|
621
|
+
launcher.project_path = project
|
|
622
|
+
|
|
623
|
+
first = self._launch(launcher, "First", results)
|
|
624
|
+
self.assertFalse(
|
|
625
|
+
first["resolved_beside"],
|
|
626
|
+
"a clip the lookup finds in Assets/Audio was extracted beside the model",
|
|
627
|
+
)
|
|
628
|
+
self.assertEqual(first["resolved_clip"], "Assets/Audio/footstep.wav", first)
|
|
629
|
+
self.assertTrue(first["hit_beside"], "premise: a missing clip is extracted")
|
|
630
|
+
|
|
631
|
+
# The user touches the extracted clip up; the next session reimports.
|
|
632
|
+
shutil.copy2(touched, os.path.join(project, "Assets", "Ghosted", "hit.wav"))
|
|
633
|
+
second = self._launch(launcher, "Second", results)
|
|
634
|
+
self.assertTrue(
|
|
635
|
+
second["touched_kept"],
|
|
636
|
+
"a later session's reimport extracted the FBX again over the touched-up "
|
|
637
|
+
"clip",
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
459
641
|
if __name__ == "__main__":
|
|
460
642
|
logging.basicConfig(level=logging.INFO)
|
|
461
643
|
unittest.main()
|
|
@@ -290,8 +290,7 @@ class FileToUnityBridge(ptk.HandoffBridge):
|
|
|
290
290
|
those export a live selection then deliver; this one takes a model already on
|
|
291
291
|
disk. It shares the exact same delivery Strategy -- :class:`CopyToAssetsDeliverer`
|
|
292
292
|
(copy into ``Assets/`` + optional Editor launch) -- so the standalone Unity
|
|
293
|
-
Workflow panel,
|
|
294
|
-
then hands the path here), a CLI, or a test all reuse one engine.
|
|
293
|
+
Workflow panel, a CLI, or a test all reuse one engine.
|
|
295
294
|
|
|
296
295
|
Set :attr:`project_path` to the target Unity project (the folder containing
|
|
297
296
|
``Assets/``) before calling :meth:`send`::
|
|
@@ -72,7 +72,10 @@ public static partial class ArticulationRecord
|
|
|
72
72
|
public string name;
|
|
73
73
|
/// <summary>An earlier joint's index, or null for a root (its parent is the rig's space).</summary>
|
|
74
74
|
public int? parent;
|
|
75
|
-
/// <summary>
|
|
75
|
+
/// <summary>
|
|
76
|
+
/// The rest translate in the parent's space, in the file's units (a runtime
|
|
77
|
+
/// measures its scale against the rest pose: ArticulationModel.scale_of).
|
|
78
|
+
/// </summary>
|
|
76
79
|
public double[] t;
|
|
77
80
|
/// <summary>The rest rotation (the DCC's joint orient), a quaternion x, y, z, w.</summary>
|
|
78
81
|
public double[] q;
|
|
@@ -102,7 +105,7 @@ public static partial class ArticulationRecord
|
|
|
102
105
|
{
|
|
103
106
|
/// <summary>
|
|
104
107
|
/// rx/ry/rz turn about the rest frame's own axes (degrees); tx/ty/tz slide along
|
|
105
|
-
/// them (the parent's
|
|
108
|
+
/// them (the file's units, in the parent's space).
|
|
106
109
|
/// </summary>
|
|
107
110
|
public string channel;
|
|
108
111
|
/// <summary>The lower limit, or null for none.</summary>
|
|
@@ -240,10 +240,10 @@ public class AudioEventImporter : AssetPostprocessor
|
|
|
240
240
|
{
|
|
241
241
|
// Fresh-import reset. OnPostprocessModel normally cleans this up, but
|
|
242
242
|
// an import that aborts between the callbacks (another postprocessor
|
|
243
|
-
// throwing) strands
|
|
244
|
-
// so
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
// throwing) strands a manifest here — Step 1 keeps the first one it
|
|
244
|
+
// meets, so a stranded one could outrank this import's own, or
|
|
245
|
+
// resurrect on a re-export without the carrier.
|
|
246
|
+
ClearPending();
|
|
247
247
|
|
|
248
248
|
// String channel: user properties arrive with Unity's default
|
|
249
249
|
// importer settings (probe-verified) — nothing to configure here.
|
|
@@ -274,20 +274,49 @@ public class AudioEventImporter : AssetPostprocessor
|
|
|
274
274
|
if (!CarrierImport.TryGetProp(propNames, values, ATTR_MANIFEST, out var manifest))
|
|
275
275
|
return;
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
// ONE manifest per model, by the readers' precedence: the scene's own
|
|
278
|
+
// carrier, else the first met (CarrierImport.Collect, as every other
|
|
279
|
+
// channel importer and the GLB applier read). An assembly ships a
|
|
280
|
+
// stray data_export1 or a module's NS:data_export beside the scene's
|
|
281
|
+
// own, each holding its events as last exported alone, while the
|
|
282
|
+
// scene's own is re-derived from the whole merged scene: concatenated,
|
|
283
|
+
// the stale events were bound and fired.
|
|
284
|
+
CarrierImport.Collect(_pendingManifestJson, assetPath, go, manifest);
|
|
285
|
+
_pendingManifests.Remove(assetPath); // parsed again from the one kept
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
// --- Step 2: Inject AnimationEvents onto clips ----------------------
|
|
283
289
|
|
|
284
|
-
// Per-assetPath
|
|
285
|
-
//
|
|
290
|
+
// Per-assetPath manifest JSON Step 1 kept (CarrierImport.Collect), and its
|
|
291
|
+
// entries parsed once on first use (PendingEntries). Drained by
|
|
286
292
|
// OnPostprocessAnimation (per clip) and OnPostprocessModel (final
|
|
287
293
|
// controller attach + cleanup).
|
|
294
|
+
static readonly Dictionary<string, string>
|
|
295
|
+
_pendingManifestJson = new Dictionary<string, string>();
|
|
288
296
|
static readonly Dictionary<string, List<ManifestEntry>>
|
|
289
297
|
_pendingManifests = new Dictionary<string, List<ManifestEntry>>();
|
|
290
298
|
|
|
299
|
+
/// <summary>The entries of the manifest Step 1 kept for this asset, or
|
|
300
|
+
/// null when it carries none.</summary>
|
|
301
|
+
List<ManifestEntry> PendingEntries()
|
|
302
|
+
{
|
|
303
|
+
if (_pendingManifests.TryGetValue(assetPath, out var entries))
|
|
304
|
+
return entries;
|
|
305
|
+
if (!_pendingManifestJson.TryGetValue(assetPath, out var json))
|
|
306
|
+
return null;
|
|
307
|
+
entries = ParseManifest(json, assetPath);
|
|
308
|
+
_pendingManifests[assetPath] = entries;
|
|
309
|
+
return entries;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// <summary>Forget this asset's pending manifest, kept and parsed.</summary>
|
|
313
|
+
void ClearPending()
|
|
314
|
+
{
|
|
315
|
+
_pendingManifestJson.Remove(assetPath);
|
|
316
|
+
_pendingManifests.Remove(assetPath);
|
|
317
|
+
_importedClips.Remove(assetPath);
|
|
318
|
+
}
|
|
319
|
+
|
|
291
320
|
// Per-assetPath clips this import built, by name: the sequence clip is
|
|
292
321
|
// only known in OnPostprocessModel (PlaceOnSequenceClip), after every
|
|
293
322
|
// clip has passed through here.
|
|
@@ -318,8 +347,8 @@ public class AudioEventImporter : AssetPostprocessor
|
|
|
318
347
|
|
|
319
348
|
bool modified = removedStale > 0;
|
|
320
349
|
|
|
321
|
-
|
|
322
|
-
|
|
350
|
+
var entries = PendingEntries();
|
|
351
|
+
if (entries != null && entries.Count > 0)
|
|
323
352
|
{
|
|
324
353
|
float frameRate = clip.frameRate > 0 ? clip.frameRate : 30f;
|
|
325
354
|
foreach (var entry in entries)
|
|
@@ -381,13 +410,12 @@ public class AudioEventImporter : AssetPostprocessor
|
|
|
381
410
|
{
|
|
382
411
|
if (!ShouldProcess())
|
|
383
412
|
{
|
|
384
|
-
|
|
385
|
-
_importedClips.Remove(assetPath);
|
|
413
|
+
ClearPending();
|
|
386
414
|
return;
|
|
387
415
|
}
|
|
388
416
|
|
|
389
|
-
|
|
390
|
-
|
|
417
|
+
var entries = PendingEntries();
|
|
418
|
+
bool hasEntries = entries != null && entries.Count > 0;
|
|
391
419
|
var existingController = root.GetComponent<AudioEventController>();
|
|
392
420
|
|
|
393
421
|
if (hasEntries)
|
|
@@ -402,10 +430,12 @@ public class AudioEventImporter : AssetPostprocessor
|
|
|
402
430
|
// file under its event's name (pythontk
|
|
403
431
|
// FbxMedia.embed_dependencies): extracted beside the model, the
|
|
404
432
|
// first folder the lookup below searches, on the one reimport
|
|
405
|
-
// that brings.
|
|
433
|
+
// that brings. A clip that lookup already finds elsewhere (an
|
|
434
|
+
// Assets/Audio library) is not asked for.
|
|
406
435
|
var names = new List<string>();
|
|
407
436
|
foreach (var entry in entries) names.Add(entry.eventName);
|
|
408
|
-
bool extracting = CarrierImport.RequestEmbeddedFiles(
|
|
437
|
+
bool extracting = CarrierImport.RequestEmbeddedFiles(
|
|
438
|
+
assetPath, names, n => FindAudioClip(n, assetPath) != null);
|
|
409
439
|
|
|
410
440
|
var seen = new HashSet<string>(
|
|
411
441
|
System.StringComparer.OrdinalIgnoreCase);
|
|
@@ -434,8 +464,7 @@ public class AudioEventImporter : AssetPostprocessor
|
|
|
434
464
|
+ root.name + "' (no audio_manifest in re-exported FBX)");
|
|
435
465
|
}
|
|
436
466
|
|
|
437
|
-
|
|
438
|
-
_importedClips.Remove(assetPath);
|
|
467
|
+
ClearPending();
|
|
439
468
|
}
|
|
440
469
|
|
|
441
470
|
// --- Step 3b: shot events onto the sequence clip -------------------
|
|
@@ -182,7 +182,7 @@ Or for objects already placed in a scene:
|
|
|
182
182
|
|---|---|---|
|
|
183
183
|
| `UnitytkSettings.cs` | — (editor-only) | The shared, compile-coupled home: the **Project Settings ▸ unitytk** page (per-channel toggles + FBX allowlist, stored in `ProjectSettings/UnitytkSettings.json`), the `ImportGate` every importer asks before doing any work, and the `CarrierImport` channel-reading helpers. |
|
|
184
184
|
| `RenderEffects/RenderEffectsController.cs` | Fades object alpha via MaterialPropertyBlock. Auto-hides at zero opacity. | Detects Maya `opacity` attribute, adds component, converts visibility curves to smooth fades. Switches a fading object's material to its pipeline's alpha blend (Built-in Fade, URP Transparent/Alpha, HDRP Transparent) and drops a pulsing highlight's emission map, as the GLB route does -- a copy when an object that needs the original shares it, and a material remapped in from the project is never edited. |
|
|
185
|
-
| `AudioEvent/AudioEventController.cs` | Plays audio clips from AnimationEvent callbacks. | Detects Maya `audio_manifest` attribute on the `data_export` carrier (baked from the `data_internal` authoring state), adds the component to the prefab root (where the Animator goes), injects AnimationEvents (in a Shots + Full Sequence file, each shot's events onto the whole-timeline clip too, at the shot's window -- needs the Shot Metadata component deployed), matches audio clips by name (an FBX that embeds them has them extracted beside it first). |
|
|
185
|
+
| `AudioEvent/AudioEventController.cs` | Plays audio clips from AnimationEvent callbacks. | Detects Maya `audio_manifest` attribute on the `data_export` carrier (baked from the `data_internal` authoring state), adds the component to the prefab root (where the Animator goes), injects AnimationEvents (in a Shots + Full Sequence file, each shot's events onto the whole-timeline clip too, at the shot's window -- needs the Shot Metadata component deployed), matches audio clips by name (an FBX that embeds them has them extracted beside it first -- unless the lookup already finds the clip, an `Assets/Audio` library say -- once per version of the model per project). Reads the scene's own carrier only, else the first that carries a manifest, as every channel does: an assembly's stray `data_export1` holds stale events. |
|
|
186
186
|
| `ShotMetadata/ShotMetadataController.cs` | Passive per-shot lookup: `GetShot(clipName)` returns the shot's description / section / object list and its `start`/`end` frames; `TryGetSequenceWindow(clipName, ...)` gives the shot's window in seconds inside `sequenceClip`, the clip cut over the whole-timeline take. | Parses the `shot_metadata` JSON off `data_export`, attaches one component to the prefab root, joins records to imported AnimationClips **by clip name** (case-insensitive). |
|
|
187
187
|
| `LightmapMetadata/LightmapMetadataController.cs` | Holds Maya-baked lightmap bindings; **Tools → unitytk → Lightmaps → Apply Scene Lightmaps** binds them into `LightmapSettings` + per-renderer `lightmapScaleOffset` (objects sharing an atlas share one lightmap slot) and records what it bound on the controller, which binds it again whenever it is enabled -- Unity keeps no lightmap it did not bake itself, so a reopened scene or a player build would otherwise come up unlit (measured on 2022.3 and 6000.3). A re-run takes the previous bindings down first; a controller whose records changed since Apply (a re-imported model) binds nothing until Apply runs again. A bound renderer gives up its material's occlusion map (`_OcclusionStrength` 0, per renderer: the bake already holds the occlusion, as the GLB pipeline rules; `bakeReplacesOcclusion` off keeps the map, and URP's SRP Batcher for that renderer); a mesh with no uv2 is left unbound and named; a realtime or mixed light reaching the baked renderers is warned about (the bake already holds the scene's lighting). Apply also puts up the bake's reflection probe as a child `ReflectionProbe` of the model root ([the reflection probe](#the-lightmap-bakes-reflection-probe)). The applier pins the texture import settings HDR lightmaps need (Lightmap type, sRGB off, mips on, BC6H / ASTC-HDR per platform), turns off **Auto Generate** lighting so a Unity re-bake can't clobber the bindings, and warns on a Gamma color-space project, a non-uv2 `uvIndex`, or a duplicate record name with no hierarchy to tell it apart by (records carry the Maya hierarchy, and same-named objects -- two machine bodies both called `BODY` -- bind by it). | Parses the `lightmap_metadata` JSON off `data_export` (the probe's nested `box` by hand — JsonUtility cannot hold it), attaches the component, warns if `generateSecondaryUV` would overwrite the DCC's UV2. A re-export without a probe clears the record. |
|
|
188
188
|
| `ShadowPlane/ShadowPlaneController.cs` (+ `ShadowPlane.shader`, `ShadowPlaneHorizon.hlsl`) | The runtime shadow rig (mayatk/blendertk `ShadowRig`, `shadow_metadata` v2): resolves the record's `source` and `contact` by name (exact, else the namespace-stripped leaf; an ambiguous name is refused), evaluates the projection model (pythontk `ShadowProjection.model` ported in `ShadowPlaneModel`, double precision) in `LateUpdate` while `followSource` is on and both resolve — placing the plane from a runtime light — otherwise the imported keys play; writes the per-plane block (`_Rect`, `_Mode`, `_Intensity`, the horizon uniforms, the contact frame, the source) read-modify-write beside `RenderEffectsController`'s `_BaseColor.a`. `ApplyNow()` for scripts. A horizon plane's march is cached (`cacheShadow`, on by default): marched into one shared atlas only when the source, the placement or the ground moves, and sampled at one fetch a pixel. Directional sources: a `Light` shines along its +Z, a bare DCC locator along its local -Z. | Parses the `shadow_metadata` JSON off `data_export` and joins each record to every renderer **its name** denotes (exact, else the namespace-stripped leaf), rewires every plane of a rig type to **one shared, instanced material** on `m3trik/ShadowPlane` (`ShadowPlane.shader`; Built-in and URP — HDRP, or a missing shader, falls back to the per-plane pipeline unlit-transparent rewire) bound to the silhouette / atlas / `_horizon` PNGs resolved by the recorded filenames, normalises an atlased plane's UVs and shares the first plane's mesh so instancing batches, turns off shadow casting/receiving + light/reflection probes, fills the controller from the record (plus `unit_scale`; `frame_a`/`frame_b` converted from FBX axes by the importer's X mirror; the horizon block's `max_stretch` — the scale its cotangents were **baked** with — kept apart from the plane's live placement cap, falling back to it only for a map baked before that field existed), pins `*_shadow` (`alphaIsTransparency`) and `*_horizon` (data) PNG imports, and removes stale controllers when a re-exported FBX no longer carries the channel. |
|
|
@@ -286,10 +286,50 @@ public static class CarrierImport
|
|
|
286
286
|
public static void RequestTextureExtraction(string assetPath)
|
|
287
287
|
=> _needExtractedTextures.Add(assetPath);
|
|
288
288
|
|
|
289
|
-
// Model versions (path, write time, size) this
|
|
289
|
+
// Model versions (path, write time, size) this PROJECT already asked to
|
|
290
290
|
// have extracted: a name the FBX does not carry stays missing after the
|
|
291
|
-
// extraction, and must not ask again on every reimport.
|
|
292
|
-
|
|
291
|
+
// extraction, and must not ask again on every reimport. Kept in Library/
|
|
292
|
+
// (per project, never versioned) rather than for one editor session: a
|
|
293
|
+
// later session's reimport (a package update, Reimport All) asked again,
|
|
294
|
+
// and each extraction overwrote the files beside the model, a touched-up
|
|
295
|
+
// one included. One line per model, its latest version; a Library rebuild
|
|
296
|
+
// forgets it and extracts once more.
|
|
297
|
+
const string EMBEDDED_REQUESTED = "Library/unitytk/embedded_requested.txt";
|
|
298
|
+
static HashSet<string> _embeddedRequested;
|
|
299
|
+
|
|
300
|
+
/// <summary>Remember that <paramref name="assetPath"/>'s version
|
|
301
|
+
/// <paramref name="key"/> asked; false when it already had.</summary>
|
|
302
|
+
static bool RememberRequest(string assetPath, string key)
|
|
303
|
+
{
|
|
304
|
+
if (_embeddedRequested == null)
|
|
305
|
+
{
|
|
306
|
+
_embeddedRequested = new HashSet<string>(StringComparer.Ordinal);
|
|
307
|
+
try
|
|
308
|
+
{
|
|
309
|
+
if (File.Exists(EMBEDDED_REQUESTED))
|
|
310
|
+
foreach (var line in File.ReadAllLines(EMBEDDED_REQUESTED))
|
|
311
|
+
if (line.Length > 0) _embeddedRequested.Add(line);
|
|
312
|
+
}
|
|
313
|
+
catch (IOException) { }
|
|
314
|
+
catch (UnauthorizedAccessException) { }
|
|
315
|
+
}
|
|
316
|
+
if (_embeddedRequested.Contains(key)) return false;
|
|
317
|
+
string prefix = assetPath + "|";
|
|
318
|
+
bool replaced = _embeddedRequested.RemoveWhere(
|
|
319
|
+
k => k.StartsWith(prefix, StringComparison.Ordinal)) > 0;
|
|
320
|
+
_embeddedRequested.Add(key);
|
|
321
|
+
try
|
|
322
|
+
{
|
|
323
|
+
Directory.CreateDirectory(Path.GetDirectoryName(EMBEDDED_REQUESTED));
|
|
324
|
+
if (replaced)
|
|
325
|
+
File.WriteAllLines(EMBEDDED_REQUESTED, _embeddedRequested);
|
|
326
|
+
else
|
|
327
|
+
File.AppendAllText(EMBEDDED_REQUESTED, key + "\n");
|
|
328
|
+
}
|
|
329
|
+
catch (IOException) { }
|
|
330
|
+
catch (UnauthorizedAccessException) { }
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
293
333
|
|
|
294
334
|
/// <summary>
|
|
295
335
|
/// The channels of the model at <paramref name="assetPath"/> name these
|
|
@@ -299,10 +339,14 @@ public static class CarrierImport
|
|
|
299
339
|
/// when one is missing there or older than the model -- a re-export:
|
|
300
340
|
/// extraction overwrites in place, keeping each file's GUID and import
|
|
301
341
|
/// settings (measured, 2022.3 / 6000.3). A name with no extension (an
|
|
302
|
-
/// audio event's clip) matches the file of that stem.
|
|
303
|
-
/// the model
|
|
342
|
+
/// audio event's clip) matches the file of that stem. A name missing
|
|
343
|
+
/// beside the model that <paramref name="resolves"/> finds where its
|
|
344
|
+
/// importer looks (the audio lookup's Assets/Audio) is not missing:
|
|
345
|
+
/// extracting for it rebound the asset to a copy beside the model.
|
|
346
|
+
/// Once per version of the model per project; true when this call asked.
|
|
304
347
|
/// </summary>
|
|
305
|
-
public static bool RequestEmbeddedFiles(
|
|
348
|
+
public static bool RequestEmbeddedFiles(
|
|
349
|
+
string assetPath, IEnumerable<string> names, Func<string, bool> resolves = null)
|
|
306
350
|
{
|
|
307
351
|
var model = new FileInfo(assetPath);
|
|
308
352
|
if (!model.Exists) return false;
|
|
@@ -311,9 +355,17 @@ public static class CarrierImport
|
|
|
311
355
|
{
|
|
312
356
|
if (string.IsNullOrEmpty(name)) continue;
|
|
313
357
|
var copy = Beside(model.DirectoryName, Path.GetFileName(name));
|
|
314
|
-
if (copy
|
|
358
|
+
if (copy != null)
|
|
359
|
+
{
|
|
360
|
+
// Older than the model: a re-export, whose copy is replaced.
|
|
361
|
+
if (copy.LastWriteTimeUtc < model.LastWriteTimeUtc) { stale = true; break; }
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (resolves != null && resolves(name)) continue;
|
|
365
|
+
stale = true;
|
|
366
|
+
break;
|
|
315
367
|
}
|
|
316
|
-
if (!stale || !
|
|
368
|
+
if (!stale || !RememberRequest(assetPath,
|
|
317
369
|
assetPath + "|" + model.LastWriteTimeUtc.Ticks + "|" + model.Length))
|
|
318
370
|
return false;
|
|
319
371
|
RequestTextureExtraction(assetPath);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigController.cs.meta
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulatedRigPointerGrab.cs.meta
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/ArticulationRecord.g.cs.meta
RENAMED
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRGrab.cs.meta
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/ArticulatedRigXRSetup.cs.meta
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ArticulatedRig/XR/M3trik.Unitytk.XR.asmdef.meta
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupController.cs.meta
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/EmissiveGroup/EmissiveGroupsLit.shader.meta
RENAMED
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/LightmapMetadata/LightmapMetadataController.cs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs
RENAMED
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/MayaSurface/MayaSurfaceMaterialImporter.cs.meta
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/RenderEffects/RenderEffectsController.cs.meta
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitytk-0.2.1 → unitytk-0.2.2}/unitytk/templates/ShotMetadata/ShotMetadataController.cs.meta
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|