unitytk 0.0.12__tar.gz → 0.0.13__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.0.12/unitytk.egg-info → unitytk-0.0.13}/PKG-INFO +1 -1
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_asset_delivery.py +0 -1
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_audio_events_integration.py +0 -1
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_clip_name_contract_integration.py +54 -36
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_emissive_groups_integration.py +11 -11
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_fade_window_preservation.py +4 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_keyed_visibility.py +6 -6
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_launcher.py +1 -1
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_lightmap_metadata_integration.py +87 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_multi_section_audio_integration.py +9 -9
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_render_opacity_controller.py +2 -2
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_render_opacity_e2e.py +4 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_render_opacity_integration.py +3 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_shadow_plane_integration.py +14 -11
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_shared_material_standalone.py +4 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_shot_metadata_integration.py +11 -2
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_shots_audio_sidebyside_integration.py +22 -10
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_standalone_enduser.py +4 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/__init__.py +1 -1
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/LightmapMetadataController.cs +147 -12
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/README.md +4 -4
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/ShadowPlaneController.cs +2 -2
- {unitytk-0.0.12 → unitytk-0.0.13/unitytk.egg-info}/PKG-INFO +1 -1
- {unitytk-0.0.12 → unitytk-0.0.13}/LICENSE +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/MANIFEST.in +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/docs/README.md +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/pyproject.toml +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/setup.cfg +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_base_test.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_bool_to_fade_curve.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_controller_runtime_integration.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_deploy_templates.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_render_effects_e2e.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_run_tests.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_scene_builder.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_shadow_plane_runtime_integration.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/test/test_temp_projects.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/asset_delivery.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/launcher.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/scene_builder.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/template_deployer.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/AudioEventController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/EmissiveGroupController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/EmissiveGroups.hlsl +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/EmissiveGroupsLit.shader +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/M3trik.Unitytk.asmdef +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/RenderEffectsController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/ShadowPlane.shader +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/ShadowPlaneHorizon.hlsl +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/ShotMetadataController.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/UnitytkSettings.cs +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk/templates/__init__.py +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk.egg-info/SOURCES.txt +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk.egg-info/dependency_links.txt +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk.egg-info/requires.txt +0 -0
- {unitytk-0.0.12 → unitytk-0.0.13}/unitytk.egg-info/top_level.txt +0 -0
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
# coding=utf-8
|
|
3
3
|
"""Integration test: the Maya→Unity clip-name JOIN contract under stress.
|
|
4
4
|
|
|
5
|
-
The whole shots→Unity scheme hinges on one identity: the
|
|
6
|
-
imported ``AnimationClip.name`` == the ``shot_metadata`` ``clip``
|
|
7
|
-
|
|
8
|
-
``[A-Za-z0-9_]
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
The whole shots→Unity scheme hinges on one identity: the shot name == the FBX
|
|
6
|
+
take name == the imported ``AnimationClip.name`` == the ``shot_metadata`` ``clip``
|
|
7
|
+
join key. The store refuses any name outside ``ShotStore.NAME_PATTERN``
|
|
8
|
+
(``[A-Za-z0-9_]``, unique ignoring case) instead of respelling it on export, so
|
|
9
|
+
what the user typed is what ships. This test proves both halves end to end:
|
|
10
|
+
Maya refuses the names the carrier would respell, and every name it accepts
|
|
11
|
+
arrives in Unity VERBATIM -- including the forms the pre-2026-09-18 respelling
|
|
12
|
+
collapsed or stripped (a doubled ``_``, a trailing ``_``), a leading digit and
|
|
13
|
+
a leading ``_``.
|
|
11
14
|
|
|
12
15
|
If Unity ever re-sanitized clip names (e.g. stripped a leading digit), the join
|
|
13
16
|
would silently break and ``ShotMetadataController.GetShot`` would return null; this
|
|
14
17
|
test is the guard against that.
|
|
15
18
|
"""
|
|
16
19
|
import os
|
|
17
|
-
import sys
|
|
18
20
|
import json
|
|
19
21
|
import shutil
|
|
20
22
|
import unittest
|
|
@@ -36,14 +38,16 @@ from unitytk import UnityLauncher, UnityFinder, TemplateDeployer
|
|
|
36
38
|
#: any other machine while the run still reported green.
|
|
37
39
|
MAYAPY = MayaEnv.mayapy()
|
|
38
40
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
41
|
+
# Names the store accepts; each must be the clip key AND the Unity clip name,
|
|
42
|
+
# character for character.
|
|
43
|
+
NAMES = [
|
|
44
|
+
"01_Start", # leading digit
|
|
45
|
+
"Fade__In", # doubled underscore (the legacy respelling collapsed it)
|
|
46
|
+
"Wipe_Out_", # trailing underscore (the legacy respelling stripped it)
|
|
47
|
+
"_Lead", # leading underscore
|
|
48
|
+
]
|
|
49
|
+
# Names the store refuses rather than let the export respell them.
|
|
50
|
+
REFUSED = ["01 Start", "Fade-In!", "fade__in"] # space, punctuation, case twin
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
def _unity_available() -> bool:
|
|
@@ -60,8 +64,10 @@ maya.standalone.initialize(name="python")
|
|
|
60
64
|
import maya.cmds as cmds
|
|
61
65
|
import maya.mel as mel
|
|
62
66
|
|
|
67
|
+
import pythontk as ptk
|
|
63
68
|
from mayatk.anim_utils.shots._shots import ShotStore
|
|
64
69
|
from mayatk.env_utils.fbx_utils import FbxUtils
|
|
70
|
+
from mayatk.node_utils.data_nodes import DataNodes
|
|
65
71
|
|
|
66
72
|
output_dir = sys.argv[1]
|
|
67
73
|
fbx_path = os.path.join(output_dir, "clip_name_contract.fbx").replace(os.sep, "/")
|
|
@@ -77,16 +83,24 @@ for t, v in ((1, 0.0), (5, 1.0), (10, 2.0), (15, 3.0), (20, 4.0),
|
|
|
77
83
|
|
|
78
84
|
store = ShotStore()
|
|
79
85
|
ShotStore.set_active(store)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
store.define_shot(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
names = json.loads(sys.argv[2])
|
|
87
|
+
for i, name in enumerate(names):
|
|
88
|
+
store.define_shot(name, 1 + 10 * i, 10 + 10 * i)
|
|
89
|
+
refused = []
|
|
90
|
+
for name in json.loads(sys.argv[3]):
|
|
91
|
+
try:
|
|
92
|
+
store.define_shot(name, 41, 45)
|
|
93
|
+
except ValueError:
|
|
94
|
+
refused.append(name)
|
|
86
95
|
|
|
87
96
|
view = store.to_export_view()
|
|
88
97
|
store.publish_export_view()
|
|
89
98
|
n_takes = FbxUtils.apply_takes_from_node()
|
|
99
|
+
# The take list the split realizes, read back off the carrier through the ONE
|
|
100
|
+
# reader of it (each shot_metadata clip's own range).
|
|
101
|
+
takes = ptk.SceneRecords.declared_takes(
|
|
102
|
+
lambda key: ptk.SceneRecords.resolve(key).load(DataNodes)
|
|
103
|
+
)
|
|
90
104
|
|
|
91
105
|
if not cmds.pluginInfo("fbxmaya", query=True, loaded=True):
|
|
92
106
|
cmds.loadPlugin("fbxmaya")
|
|
@@ -98,7 +112,9 @@ results = {
|
|
|
98
112
|
"fbx_path": fbx_path,
|
|
99
113
|
"fbx_exists": os.path.exists(fbx_path),
|
|
100
114
|
"n_takes": n_takes,
|
|
101
|
-
"clip_keys": [t["name"] for t in
|
|
115
|
+
"clip_keys": [t["name"] for t in takes],
|
|
116
|
+
"meta_keys": [s["clip"] for s in view["shot_metadata"]["shots"]],
|
|
117
|
+
"refused": refused,
|
|
102
118
|
}
|
|
103
119
|
with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
|
|
104
120
|
json.dump(results, f, indent=2)
|
|
@@ -214,7 +230,7 @@ class TestClipNameContract(unittest.TestCase):
|
|
|
214
230
|
)
|
|
215
231
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
216
232
|
proc = subprocess.run(
|
|
217
|
-
[MAYAPY, script_path, self.temp_dir],
|
|
233
|
+
[MAYAPY, script_path, self.temp_dir, json.dumps(NAMES), json.dumps(REFUSED)],
|
|
218
234
|
capture_output=True, text=True, timeout=180, env=env,
|
|
219
235
|
)
|
|
220
236
|
if proc.stderr:
|
|
@@ -226,9 +242,12 @@ class TestClipNameContract(unittest.TestCase):
|
|
|
226
242
|
r = self.__class__.maya_results
|
|
227
243
|
self.__class__.fbx_path = r["fbx_path"]
|
|
228
244
|
self.assertTrue(r["fbx_exists"])
|
|
229
|
-
self.assertEqual(r["n_takes"],
|
|
230
|
-
# Maya half
|
|
231
|
-
|
|
245
|
+
self.assertEqual(r["n_takes"], len(NAMES))
|
|
246
|
+
# The Maya half: every refused name was refused, every accepted name
|
|
247
|
+
# is its own take and join key, unchanged and in order.
|
|
248
|
+
self.assertEqual(r["refused"], REFUSED)
|
|
249
|
+
self.assertEqual(r["clip_keys"], NAMES)
|
|
250
|
+
self.assertEqual(r["meta_keys"], NAMES)
|
|
232
251
|
|
|
233
252
|
@MayaEnv.skip_unless_available()
|
|
234
253
|
@EnvSkip.unless(_unity_available(), "no Unity editor found")
|
|
@@ -304,18 +323,17 @@ class TestClipNameContract(unittest.TestCase):
|
|
|
304
323
|
clip_names = set(unity.get("clip_names", []))
|
|
305
324
|
clip_keys = set(unity.get("clip_keys", []))
|
|
306
325
|
|
|
307
|
-
# Every
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
for sanitized in EXPECTED.values():
|
|
326
|
+
# Every authored name imported as a clip with that exact name. (Unity
|
|
327
|
+
# also imports a default "Take 001" clip for the full baked timeline; it
|
|
328
|
+
# carries no shot record and is ignored by the join — so this is a
|
|
329
|
+
# superset check, not equality.)
|
|
330
|
+
for name in NAMES:
|
|
313
331
|
self.assertIn(
|
|
314
|
-
|
|
315
|
-
f"Unity altered/dropped clip {
|
|
332
|
+
name, clip_names,
|
|
333
|
+
f"Unity altered/dropped clip {name!r}; imported: {sorted(clip_names)}",
|
|
316
334
|
)
|
|
317
|
-
# Metadata keys are exactly the
|
|
318
|
-
self.assertEqual(clip_keys, set(
|
|
335
|
+
# Metadata keys are exactly the authored shots, and each joins to a clip.
|
|
336
|
+
self.assertEqual(clip_keys, set(NAMES))
|
|
319
337
|
for key in clip_keys:
|
|
320
338
|
self.assertIn(key, clip_names, f"Join broken: key {key!r} has no clip.")
|
|
321
339
|
|
|
@@ -7,9 +7,9 @@ transports, curve rebinding, proxy-node deletion) and was the only one with no
|
|
|
7
7
|
batch-Unity coverage at all. This closes the import-side half:
|
|
8
8
|
|
|
9
9
|
1. Maya (mayapy): cube → two real ``EmissiveGroups.add_group`` calls with
|
|
10
|
-
distinct default weights → ``FbxUtils.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
distinct default weights → ``FbxUtils.publish()`` (the one publish of
|
|
11
|
+
every producer the Scene Exporter makes, validating the
|
|
12
|
+
``FbxUtils.PRODUCERS`` wiring) → export mesh + ``data_export`` carrier.
|
|
13
13
|
Then clear the groups and export a SECOND FBX with no channel.
|
|
14
14
|
2. Unity (batchmode): import the first FBX with the real templates
|
|
15
15
|
deployed and verify the populated ``EmissiveGroupController`` on the
|
|
@@ -30,7 +30,6 @@ Requires:
|
|
|
30
30
|
- Unity Hub with at least one editor installed (skips on license wall)
|
|
31
31
|
"""
|
|
32
32
|
import os
|
|
33
|
-
import sys
|
|
34
33
|
import json
|
|
35
34
|
import shutil
|
|
36
35
|
import unittest
|
|
@@ -70,6 +69,7 @@ import maya.standalone
|
|
|
70
69
|
maya.standalone.initialize(name="python")
|
|
71
70
|
import maya.cmds as cmds
|
|
72
71
|
|
|
72
|
+
import pythontk as ptk
|
|
73
73
|
from mayatk.mat_utils.emissive_groups import EmissiveGroups
|
|
74
74
|
from mayatk.env_utils.fbx_utils import FbxUtils
|
|
75
75
|
from mayatk.node_utils.data_nodes import DataNodes
|
|
@@ -86,10 +86,10 @@ EmissiveGroups.add_group("taillights", faces=[cube + ".f[2:3]"], default=0.25)
|
|
|
86
86
|
|
|
87
87
|
warnings = EmissiveGroups.validate()
|
|
88
88
|
|
|
89
|
-
# The
|
|
90
|
-
#
|
|
91
|
-
FbxUtils.
|
|
92
|
-
manifest = DataNodes.
|
|
89
|
+
# The one publish of every producer -- exactly what the Scene Exporter makes
|
|
90
|
+
# before an export (validates the FbxUtils.PRODUCERS wiring).
|
|
91
|
+
FbxUtils.publish()
|
|
92
|
+
manifest = DataNodes.read(ptk.Scope.DELIVERABLE, EmissiveGroups.DATA_CHANNEL)
|
|
93
93
|
|
|
94
94
|
if not cmds.pluginInfo("fbxmaya", query=True, loaded=True):
|
|
95
95
|
cmds.loadPlugin("fbxmaya")
|
|
@@ -109,8 +109,8 @@ _export(with_path)
|
|
|
109
109
|
# Second export with the channel gone — drives the stale-removal path.
|
|
110
110
|
for name in list(EmissiveGroups.list_groups()):
|
|
111
111
|
EmissiveGroups.remove_group(name)
|
|
112
|
-
FbxUtils.
|
|
113
|
-
cleared = DataNodes.
|
|
112
|
+
FbxUtils.publish()
|
|
113
|
+
cleared = DataNodes.read(ptk.Scope.DELIVERABLE, EmissiveGroups.DATA_CHANNEL)
|
|
114
114
|
|
|
115
115
|
without_path = os.path.join(output_dir, "Lamp_emissive_cleared.fbx")
|
|
116
116
|
_export(without_path)
|
|
@@ -233,7 +233,7 @@ class TestEmissiveGroupsIntegration(unittest.TestCase):
|
|
|
233
233
|
|
|
234
234
|
@MayaEnv.skip_unless_available()
|
|
235
235
|
def test_01_maya_export(self):
|
|
236
|
-
"""cube → two add_group calls →
|
|
236
|
+
"""cube → two add_group calls → FbxUtils.publish → FBX with carrier."""
|
|
237
237
|
script_path = os.path.join(self.temp_dir, "maya_export.py")
|
|
238
238
|
with open(script_path, "w", encoding="utf-8") as f:
|
|
239
239
|
f.write(MAYA_EXPORT_SCRIPT)
|
|
@@ -57,7 +57,7 @@ import maya.standalone
|
|
|
57
57
|
maya.standalone.initialize(name="python")
|
|
58
58
|
import maya.cmds as cmds
|
|
59
59
|
import maya.mel as mel
|
|
60
|
-
from mayatk import
|
|
60
|
+
from mayatk import RenderEffects
|
|
61
61
|
|
|
62
62
|
output_dir = sys.argv[1]
|
|
63
63
|
fbx_path = os.path.join(output_dir, "fade_window.fbx")
|
|
@@ -68,12 +68,12 @@ cmds.currentUnit(time="film")
|
|
|
68
68
|
cmds.playbackOptions(min=1, max=160)
|
|
69
69
|
|
|
70
70
|
cube = cmds.polyCube(name="WindowCube")[0]
|
|
71
|
-
|
|
71
|
+
RenderEffects.create(objects=[cube], mode="attribute")
|
|
72
72
|
|
|
73
73
|
# Short fade-in: frame 1 → 11 (10 frames, 0.417s @ 24fps)
|
|
74
|
-
|
|
74
|
+
RenderEffects.key_fade(objects=[cube], start=1, end=11, direction="in")
|
|
75
75
|
# Long fade-out: frame 91 → 151 (60 frames, 2.5s @ 24fps)
|
|
76
|
-
|
|
76
|
+
RenderEffects.key_fade(objects=[cube], start=91, end=151, direction="out")
|
|
77
77
|
|
|
78
78
|
# Add a translate carrier so the FBX has a clip spanning the timeline
|
|
79
79
|
cmds.setKeyframe(cube, attribute="translateY", time=1, value=0)
|
|
@@ -523,7 +523,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
523
523
|
logger.error(f"Unity log tail:\n{tail}")
|
|
524
524
|
# Check for compilation errors
|
|
525
525
|
if "error CS" in log_content:
|
|
526
|
-
errors = [
|
|
526
|
+
errors = [ln for ln in log_content.split("\n") if "error CS" in ln]
|
|
527
527
|
for e in errors[:10]:
|
|
528
528
|
logger.error(f" {e.strip()}")
|
|
529
529
|
|
|
@@ -545,7 +545,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
545
545
|
self.assertTrue(r.get("import_visibility"), "importVisibility was not enabled")
|
|
546
546
|
self.assertTrue(r.get("import_animation"), "importAnimation was not enabled")
|
|
547
547
|
self.assertFalse(r.get("resample_curves"), "resampleCurves should be OFF")
|
|
548
|
-
print(
|
|
548
|
+
print(" Import Settings: visibility=ON, animation=ON, resample=OFF")
|
|
549
549
|
|
|
550
550
|
# Prefab
|
|
551
551
|
self.assertTrue(r.get("prefab_loaded"), "FBX prefab failed to load")
|
|
@@ -626,7 +626,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
626
626
|
print(f" Found with curves: {len(found)}")
|
|
627
627
|
print(f" MISSING curves: {len(missing)}")
|
|
628
628
|
if missing:
|
|
629
|
-
print(
|
|
629
|
+
print("\n *** MISSING VISIBILITY CURVES ***")
|
|
630
630
|
for m in missing:
|
|
631
631
|
in_hierarchy = any(m in obj for obj in loc_objects)
|
|
632
632
|
status = "IN HIERARCHY" if in_hierarchy else "NOT IN FBX"
|
|
@@ -638,7 +638,7 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
638
638
|
if sample_error:
|
|
639
639
|
print(f"\n Sampling Error: {sample_error}")
|
|
640
640
|
elif sample_results:
|
|
641
|
-
print(
|
|
641
|
+
print("\n AnimationMode Sampling:")
|
|
642
642
|
for sr in sample_results:
|
|
643
643
|
print(f" {sr}")
|
|
644
644
|
|
|
@@ -657,8 +657,8 @@ class TestKeyedVisibility(unittest.TestCase):
|
|
|
657
657
|
print(
|
|
658
658
|
f"\n WARNING: {len(missing)} expected objects missing visibility curves"
|
|
659
659
|
)
|
|
660
|
-
print(
|
|
661
|
-
print(
|
|
660
|
+
print(" These objects may need .visibility keyed directly in Maya")
|
|
661
|
+
print(" (display layer visibility does NOT export to FBX)")
|
|
662
662
|
|
|
663
663
|
print("\n VERDICT: See results above for visibility import status.")
|
|
664
664
|
print("=" * 70 + "\n")
|
|
@@ -27,7 +27,6 @@ Requires:
|
|
|
27
27
|
- Unity Hub with at least one editor installed
|
|
28
28
|
"""
|
|
29
29
|
import os
|
|
30
|
-
import sys
|
|
31
30
|
import json
|
|
32
31
|
import shutil
|
|
33
32
|
import unittest
|
|
@@ -68,6 +67,7 @@ maya.standalone.initialize(name="python")
|
|
|
68
67
|
import maya.cmds as cmds
|
|
69
68
|
import maya.mel as mel
|
|
70
69
|
|
|
70
|
+
import pythontk as ptk
|
|
71
71
|
from mayatk.light_utils.lightmap_baker.lightmap_baker import LightmapBaker
|
|
72
72
|
from mayatk.uv_utils._uv_utils import UvUtils
|
|
73
73
|
from mayatk.node_utils.data_nodes import DataNodes
|
|
@@ -104,7 +104,7 @@ except Exception:
|
|
|
104
104
|
|
|
105
105
|
long = cmds.ls(cube, long=True)[0]
|
|
106
106
|
baker = LightmapBaker(resolution=64)
|
|
107
|
-
baker.commit_lightmap({long: tex_path}
|
|
107
|
+
baker.commit_lightmap({long: tex_path})
|
|
108
108
|
|
|
109
109
|
# Material must be untouched (the whole point of lighting-only mode).
|
|
110
110
|
sgs_after = cmds.listConnections(shape, type="shadingEngine") or []
|
|
@@ -124,7 +124,9 @@ results = {
|
|
|
124
124
|
"fbx_exists": os.path.exists(fbx_path),
|
|
125
125
|
"material_kept": material_kept,
|
|
126
126
|
"real_exr": real_exr,
|
|
127
|
-
"metadata": json.loads(
|
|
127
|
+
"metadata": json.loads(
|
|
128
|
+
DataNodes.read(ptk.Scope.DELIVERABLE, LightmapBaker.LIGHTMAP_METADATA)
|
|
129
|
+
),
|
|
128
130
|
}
|
|
129
131
|
with open(os.path.join(output_dir, "maya_results.json"), "w") as f:
|
|
130
132
|
json.dump(results, f, indent=2)
|
|
@@ -284,7 +286,76 @@ public class LightmapMetaTestVerifier
|
|
|
284
286
|
+ (nonDirectional ? "true," : "false,"));
|
|
285
287
|
sb.AppendLine(" \"tex_type_lightmap\": " + (texLightmap ? "true," : "false,"));
|
|
286
288
|
sb.AppendLine(" \"tex_srgb_off\": " + (texSrgbOff ? "true," : "false,"));
|
|
287
|
-
sb.AppendLine(" \"tex_mips_on\": " + (texMips ? "true" : "false"));
|
|
289
|
+
sb.AppendLine(" \"tex_mips_on\": " + (texMips ? "true," : "false,"));
|
|
290
|
+
|
|
291
|
+
// Same-named objects bind by hierarchy, as ONE assignment (the rule
|
|
292
|
+
// pythontk's GLB applier follows): FBX carries leaf names only, so a
|
|
293
|
+
// production room's two machine bodies are both BODY, and the
|
|
294
|
+
// manifest is the whole SCENE's, so a partial export carries records
|
|
295
|
+
// whose objects are not here. Built in memory; the REAL (private)
|
|
296
|
+
// ResolveRenderers settles each case.
|
|
297
|
+
string byHierarchy = "not run";
|
|
298
|
+
try
|
|
299
|
+
{
|
|
300
|
+
Func<string, Transform, bool, Transform> node = (nodeName, under, withRenderer) =>
|
|
301
|
+
{
|
|
302
|
+
var go = new GameObject(nodeName);
|
|
303
|
+
go.transform.SetParent(under, false);
|
|
304
|
+
if (withRenderer) go.AddComponent<MeshRenderer>();
|
|
305
|
+
return go.transform;
|
|
306
|
+
};
|
|
307
|
+
var resolve = typeof(LightmapMetadataApplier).GetMethod("ResolveRenderers",
|
|
308
|
+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
|
309
|
+
// -> (bound, ambiguous) for these records under that root.
|
|
310
|
+
Func<Transform, LightmapRecord[], Tuple<Dictionary<LightmapRecord, Renderer>, HashSet<LightmapRecord>>> call =
|
|
311
|
+
(under, recs) =>
|
|
312
|
+
{
|
|
313
|
+
var args = new object[] { under, new List<LightmapRecord>(recs), null };
|
|
314
|
+
var map = (Dictionary<LightmapRecord, Renderer>)resolve.Invoke(null, args);
|
|
315
|
+
return Tuple.Create(map, (HashSet<LightmapRecord>)args[2]);
|
|
316
|
+
};
|
|
317
|
+
var root = new GameObject("fr_root").transform;
|
|
318
|
+
var lxLeaf = node("BODY", node("BODY", node("MACHINE_B", root, false), false), true)
|
|
319
|
+
.GetComponent<Renderer>();
|
|
320
|
+
var alLeaf = node("BODY", node("MACHINE_A", root, false), true)
|
|
321
|
+
.GetComponent<Renderer>();
|
|
322
|
+
var lx = new LightmapRecord { name = "BODY", hierarchy = new[] { "room", "MACHINE_B", "BODY", "BODY" } };
|
|
323
|
+
var al = new LightmapRecord { name = "BODY", hierarchy = new[] { "room", "MACHINE_A", "BODY" } };
|
|
324
|
+
var vend = new LightmapRecord { name = "BODY", hierarchy = new[] { "room", "VENDING", "BODY" } };
|
|
325
|
+
|
|
326
|
+
// Both bodies bind their own; a third BODY whose object is not
|
|
327
|
+
// here binds nothing and is not called ambiguous either.
|
|
328
|
+
var full = call(root, new[] { lx, al, vend });
|
|
329
|
+
bool own = full.Item1.TryGetValue(lx, out var r1) && r1 == lxLeaf
|
|
330
|
+
&& full.Item1.TryGetValue(al, out var r2) && r2 == alLeaf;
|
|
331
|
+
bool absent = !full.Item1.ContainsKey(vend) && !full.Item2.Contains(vend);
|
|
332
|
+
|
|
333
|
+
// A partial export: only the MACHINE_A is here. The MACHINE_B's
|
|
334
|
+
// record found only that BODY and took it, overwriting its map.
|
|
335
|
+
var solo = new GameObject("fr_solo").transform;
|
|
336
|
+
var soloLeaf = node("BODY", node("MACHINE_A", solo, false), true)
|
|
337
|
+
.GetComponent<Renderer>();
|
|
338
|
+
var part = call(solo, new[] { lx, al });
|
|
339
|
+
bool partial = part.Item1.Count == 1
|
|
340
|
+
&& part.Item1.TryGetValue(al, out var r3) && r3 == soloLeaf;
|
|
341
|
+
|
|
342
|
+
// Same-named records with no hierarchy are never guessed.
|
|
343
|
+
var bare = call(root, new[] {
|
|
344
|
+
new LightmapRecord { name = "BODY" }, new LightmapRecord { name = "BODY" } });
|
|
345
|
+
bool unguessed = bare.Item1.Count == 0 && bare.Item2.Count == 2;
|
|
346
|
+
|
|
347
|
+
byHierarchy = (own ? "ok" : "bodies missed") + "|"
|
|
348
|
+
+ (absent ? "ok" : "an absent record bound") + "|"
|
|
349
|
+
+ (partial ? "ok" : "partial export mis-bound") + "|"
|
|
350
|
+
+ (unguessed ? "ok" : "no-hierarchy duplicates guessed");
|
|
351
|
+
UnityEngine.Object.DestroyImmediate(root.gameObject);
|
|
352
|
+
UnityEngine.Object.DestroyImmediate(solo.gameObject);
|
|
353
|
+
}
|
|
354
|
+
catch (Exception e)
|
|
355
|
+
{
|
|
356
|
+
byHierarchy = "raised: " + e.GetType().Name + " " + e.Message;
|
|
357
|
+
}
|
|
358
|
+
sb.AppendLine(" \"find_by_hierarchy\": \"" + Escape(byHierarchy) + "\"");
|
|
288
359
|
|
|
289
360
|
sb.AppendLine("}");
|
|
290
361
|
|
|
@@ -366,6 +437,11 @@ class TestLightmapMetadataIntegration(unittest.TestCase):
|
|
|
366
437
|
self.assertIn("Crate", objs)
|
|
367
438
|
self.assertEqual(objs["Crate"]["map"], "Crate_Lightmap.exr")
|
|
368
439
|
self.assertEqual(objs["Crate"]["uvIndex"], 1)
|
|
440
|
+
# The map is a join token by FILE NAME: the manifest carries no
|
|
441
|
+
# authoring folder (the absolute ``dir`` / ``dirs`` it once published
|
|
442
|
+
# resolved nowhere but the baking machine).
|
|
443
|
+
self.assertNotIn("dir", r["metadata"])
|
|
444
|
+
self.assertNotIn("dirs", r["metadata"])
|
|
369
445
|
|
|
370
446
|
# ---- Step 2: Unity import ----------------------------------------
|
|
371
447
|
|
|
@@ -499,6 +575,13 @@ class TestLightmapMetadataIntegration(unittest.TestCase):
|
|
|
499
575
|
self.assertTrue(unity.get("tex_srgb_off"), "sRGB not pinned off")
|
|
500
576
|
self.assertTrue(unity.get("tex_mips_on"), "mipmaps not pinned on")
|
|
501
577
|
|
|
578
|
+
# (e) same-named objects bind by hierarchy, not first match (the
|
|
579
|
+
# production room: MACHINE_B|BODY|BODY beside MACHINE_A|BODY); a record
|
|
580
|
+
# without a hierarchy, as older manifests carry, still takes the first.
|
|
581
|
+
self.assertEqual(
|
|
582
|
+
unity.get("find_by_hierarchy"), "ok|ok|ok|ok", unity.get("find_by_hierarchy")
|
|
583
|
+
)
|
|
584
|
+
|
|
502
585
|
|
|
503
586
|
if __name__ == "__main__":
|
|
504
587
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -903,7 +903,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
903
903
|
audio_stems,
|
|
904
904
|
f"{obj_name}: clip '{name}' not in source audio dir",
|
|
905
905
|
)
|
|
906
|
-
print(
|
|
906
|
+
print(" Clip names verified against source audio directory")
|
|
907
907
|
|
|
908
908
|
# ================================================================
|
|
909
909
|
# 3. RenderEffectsController — components + Renderer gating
|
|
@@ -917,7 +917,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
917
917
|
print(f" With Renderer: {oc_with_rend}")
|
|
918
918
|
print(f" Without Renderer: {oc_without_rend}")
|
|
919
919
|
if oc_details:
|
|
920
|
-
print(
|
|
920
|
+
print(" Details (first 5):")
|
|
921
921
|
for d in oc_details[:5]:
|
|
922
922
|
print(f" {d}")
|
|
923
923
|
if len(oc_details) > 5:
|
|
@@ -960,7 +960,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
960
960
|
# ================================================================
|
|
961
961
|
binding_freq = r.get("curve_binding_freq", [])
|
|
962
962
|
if binding_freq:
|
|
963
|
-
print(
|
|
963
|
+
print("\n Curve binding frequency (top 20):")
|
|
964
964
|
for entry in binding_freq[:20]:
|
|
965
965
|
print(f" {entry}")
|
|
966
966
|
if len(binding_freq) > 20:
|
|
@@ -1023,7 +1023,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
1023
1023
|
# Find keys like "opacity_curve_samples_Take 001"
|
|
1024
1024
|
sample_keys = [k for k in r if k.startswith("opacity_curve_samples_")]
|
|
1025
1025
|
if sample_keys:
|
|
1026
|
-
print(
|
|
1026
|
+
print("\n Opacity curve samples:")
|
|
1027
1027
|
for sk in sample_keys:
|
|
1028
1028
|
samples = r[sk]
|
|
1029
1029
|
print(f" [{sk}] ({len(samples)} curves sampled)")
|
|
@@ -1082,7 +1082,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
1082
1082
|
audio_stems = set(_collect_audio_files(AUDIO_DIR))
|
|
1083
1083
|
if event_time_keys:
|
|
1084
1084
|
all_event_names_from_clip: set[str] = set()
|
|
1085
|
-
print(
|
|
1085
|
+
print("\n Audio event times:")
|
|
1086
1086
|
for etk in event_time_keys:
|
|
1087
1087
|
events = r[etk]
|
|
1088
1088
|
print(f" [{etk}] {len(events)} events")
|
|
@@ -1205,7 +1205,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
1205
1205
|
ri_oc = r.get("reimport_opacity_controllers", -1)
|
|
1206
1206
|
ri_ac = r.get("reimport_audio_controllers", -1)
|
|
1207
1207
|
|
|
1208
|
-
print(
|
|
1208
|
+
print("\n Reimport idempotency:")
|
|
1209
1209
|
print(f" Events: {total_events} -> {ri_events}")
|
|
1210
1210
|
print(f" Opa curves: {oc_curves_rebound} -> {ri_opacity}")
|
|
1211
1211
|
print(f" audio_trigger: {at_curves} -> {ri_at}")
|
|
@@ -1248,7 +1248,7 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
1248
1248
|
changed_500 = r.get("anim_sample_changed_by_t500", 0)
|
|
1249
1249
|
anim_details = r.get("anim_sample_details", [])
|
|
1250
1250
|
|
|
1251
|
-
print(
|
|
1251
|
+
print("\n AnimationMode runtime sampling:")
|
|
1252
1252
|
print(f" Controllers sampled: {anim_count}")
|
|
1253
1253
|
print(f" Changed by t=15s: {changed_15}")
|
|
1254
1254
|
print(f" Changed by t=500s: {changed_500}")
|
|
@@ -1264,8 +1264,8 @@ class TestMultiSectionAudioIntegration(unittest.TestCase):
|
|
|
1264
1264
|
self.assertGreater(
|
|
1265
1265
|
changed_15 + changed_500,
|
|
1266
1266
|
0,
|
|
1267
|
-
|
|
1268
|
-
|
|
1267
|
+
"No opacity values changed between t=0 and t=15s/t=500s — "
|
|
1268
|
+
"curves may not be driving RenderEffectsController.opacity",
|
|
1269
1269
|
)
|
|
1270
1270
|
|
|
1271
1271
|
print("=" * 70)
|
|
@@ -59,7 +59,7 @@ import maya.standalone
|
|
|
59
59
|
maya.standalone.initialize(name="python")
|
|
60
60
|
import maya.cmds as cmds
|
|
61
61
|
import maya.mel as mel
|
|
62
|
-
from mayatk import
|
|
62
|
+
from mayatk import RenderEffects
|
|
63
63
|
|
|
64
64
|
output_dir = sys.argv[1]
|
|
65
65
|
fbx_path = os.path.join(output_dir, "fade_test.fbx")
|
|
@@ -71,7 +71,7 @@ cmds.playbackOptions(min=1, max=140)
|
|
|
71
71
|
cube = cmds.polyCube(name="FadeCube")[0]
|
|
72
72
|
|
|
73
73
|
# Initialize attribute-mode fade
|
|
74
|
-
|
|
74
|
+
RenderEffects.create(objects=[cube], mode="attribute")
|
|
75
75
|
|
|
76
76
|
# Animate opacity: Fade In -> Hold -> Fade Out
|
|
77
77
|
cmds.cutKey(cube, attribute="opacity")
|
|
@@ -11,7 +11,7 @@ curve values, reimport idempotency).
|
|
|
11
11
|
Pipeline:
|
|
12
12
|
1. Maya (mayapy): build a cube + 'opacity' attr, key opacity in a
|
|
13
13
|
4-key fade-in/hold/fade-out pattern, call
|
|
14
|
-
:meth:`
|
|
14
|
+
:meth:`RenderEffects.prepare_for_export` to stage the curve proxy
|
|
15
15
|
(nothing keys visibility), export FBX.
|
|
16
16
|
2. Unity (batchmode): deploy ``UnitytkSettings.cs`` (the shared
|
|
17
17
|
ImportGate/CarrierImport home), ``RenderEffectsController.cs`` and
|
|
@@ -75,7 +75,7 @@ maya.standalone.initialize(name="python")
|
|
|
75
75
|
import maya.cmds as cmds
|
|
76
76
|
import maya.mel as mel
|
|
77
77
|
|
|
78
|
-
from mayatk.mat_utils.render_opacity.
|
|
78
|
+
from mayatk.mat_utils.render_opacity.render_effects import RenderEffects
|
|
79
79
|
|
|
80
80
|
output_dir = sys.argv[1]
|
|
81
81
|
fbx_path = os.path.join(output_dir, "opacity_test.fbx")
|
|
@@ -86,7 +86,7 @@ cmds.playbackOptions(min=1, max=140)
|
|
|
86
86
|
cube = cmds.polyCube(name="fade_cube")[0]
|
|
87
87
|
|
|
88
88
|
# Initialize attribute-mode opacity (creates the 'opacity' attr)
|
|
89
|
-
|
|
89
|
+
RenderEffects.create(objects=[cube], mode="attribute")
|
|
90
90
|
|
|
91
91
|
# Author a 4-key fade-in → hold → fade-out pattern
|
|
92
92
|
cmds.cutKey(cube, attribute="opacity")
|
|
@@ -101,7 +101,7 @@ assert cmds.attributeQuery("opacity", node=str(cube), exists=True)
|
|
|
101
101
|
# opacity curve on scale.x, which the importer rebinds to the controller
|
|
102
102
|
# (Unity flattens animated custom properties onto the root). Nothing keys
|
|
103
103
|
# visibility: presence is derived from the authored opacity downstream.
|
|
104
|
-
synced =
|
|
104
|
+
synced = RenderEffects.prepare_for_export()
|
|
105
105
|
proxies = cmds.ls("*__opacity") or []
|
|
106
106
|
|
|
107
107
|
vis_keys = cmds.keyframe(cube, attribute="visibility", q=True, tc=True) or []
|
|
@@ -14,7 +14,6 @@ Requires:
|
|
|
14
14
|
- mayatk, pythontk on PYTHONPATH
|
|
15
15
|
"""
|
|
16
16
|
import os
|
|
17
|
-
import sys
|
|
18
17
|
import json
|
|
19
18
|
import shutil
|
|
20
19
|
import unittest
|
|
@@ -63,7 +62,7 @@ import maya.cmds as cmds
|
|
|
63
62
|
import maya.mel as mel
|
|
64
63
|
|
|
65
64
|
import mayatk # bootstrap lazy-loader
|
|
66
|
-
from mayatk import
|
|
65
|
+
from mayatk import RenderEffects
|
|
67
66
|
|
|
68
67
|
output_dir = sys.argv[1] # passed from the test
|
|
69
68
|
fbx_path = os.path.join(output_dir, "fade_test.fbx")
|
|
@@ -75,7 +74,7 @@ cmds.playbackOptions(min=1, max=140)
|
|
|
75
74
|
cube = cmds.polyCube(name="FadeCube")[0]
|
|
76
75
|
|
|
77
76
|
# Initialize attribute-mode fade
|
|
78
|
-
result =
|
|
77
|
+
result = RenderEffects.create(
|
|
79
78
|
objects=[cube], mode="attribute"
|
|
80
79
|
)
|
|
81
80
|
|
|
@@ -584,7 +583,7 @@ class TestRenderOpacityIntegration(unittest.TestCase):
|
|
|
584
583
|
f"Clips found: {results.get('clips_found')}\n"
|
|
585
584
|
f"Curve names: {results.get('curve_names')}",
|
|
586
585
|
)
|
|
587
|
-
logger.info(
|
|
586
|
+
logger.info("INTEGRATION TEST PASSED — fade curve verified in Unity")
|
|
588
587
|
|
|
589
588
|
|
|
590
589
|
if __name__ == "__main__":
|