@gripforgeai/mcp 0.1.10 → 0.1.12
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.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +83 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +133 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/look.js +6 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +79 -13
- package/dist/server-tools.js +3 -3
- package/dist/server.js +139 -13
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/vfx-project-tools.js +2 -1
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +6 -1
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""GripForge read-only UE -> portable scene worker (Python commandlet).
|
|
2
|
+
|
|
3
|
+
Run in a separate editor process with rendering enabled and -GFExportJob=<dir>.
|
|
4
|
+
No source asset/map is saved. Atomic checkpoints survive the MCP request.
|
|
5
|
+
"""
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import unreal
|
|
15
|
+
|
|
16
|
+
match = re.search(r'-GFExportJob=(?:"([^"]+)"|(\S+))', unreal.SystemLibrary.get_command_line())
|
|
17
|
+
if not match:
|
|
18
|
+
raise RuntimeError('Missing -GFExportJob')
|
|
19
|
+
folder = Path(match.group(1) or match.group(2)).resolve()
|
|
20
|
+
request = json.loads((folder / 'request.json').read_text())
|
|
21
|
+
fingerprint = hashlib.sha256((folder / 'request.json').read_bytes()).hexdigest()
|
|
22
|
+
manifest_path = folder / 'native.json'
|
|
23
|
+
state_path = folder / 'status.json'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def atomic(path, value):
|
|
27
|
+
tmp = path.with_suffix(path.suffix + '.tmp')
|
|
28
|
+
tmp.write_text(json.dumps(value, separators=(',', ':')))
|
|
29
|
+
tmp.replace(path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sha(path):
|
|
33
|
+
h = hashlib.sha256()
|
|
34
|
+
with path.open('rb') as f:
|
|
35
|
+
for b in iter(lambda: f.read(1024 * 1024), b''):
|
|
36
|
+
h.update(b)
|
|
37
|
+
return h.hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cancelled():
|
|
41
|
+
p = folder / 'control.json'
|
|
42
|
+
if p.exists() and json.loads(p.read_text()).get('cancel'):
|
|
43
|
+
raise InterruptedError('Export cancelled; completed assets retained')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def progress(stage, **kw):
|
|
47
|
+
cancelled()
|
|
48
|
+
atomic(state_path, dict(schema='gripforge.unreal-export-job.v1', request_hash=fingerprint,
|
|
49
|
+
status='running', stage=stage, pid=os.getpid(), updated_at=time.time(), **kw))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def xyz(v):
|
|
53
|
+
return [float(v.x), float(v.y), float(v.z)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def transform(t):
|
|
57
|
+
q = t.rotation
|
|
58
|
+
p, s = t.translation, t.scale3d
|
|
59
|
+
return dict(translation=dict(x=p.x*.01, y=p.z*.01, z=p.y*.01),
|
|
60
|
+
rotation=dict(x=-q.x, y=-q.z, z=-q.y, w=q.w),
|
|
61
|
+
scale=dict(x=s.x, y=s.z, z=s.y))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def native_prop(obj, names, default=None):
|
|
65
|
+
for name in names:
|
|
66
|
+
try:
|
|
67
|
+
return obj.get_editor_property(name)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
return default
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def file_record(file):
|
|
74
|
+
p = folder / file
|
|
75
|
+
return dict(file=file, sha256=sha(p), bytes=p.stat().st_size)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def done(record):
|
|
79
|
+
p = folder / record['file']
|
|
80
|
+
return p.is_file() and sha(p) == record['sha256']
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def package_file(path):
|
|
84
|
+
package = path.split('.')[0]
|
|
85
|
+
if package.startswith('/Game/'):
|
|
86
|
+
return Path(unreal.Paths.project_content_dir()) / (package[6:] + '.uasset')
|
|
87
|
+
if package.startswith('/Engine/'):
|
|
88
|
+
return Path(unreal.Paths.engine_content_dir()) / (package[8:] + '.uasset')
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def png(rt, name):
|
|
93
|
+
unreal.RenderingLibrary.export_render_target(world, rt, str(folder), name)
|
|
94
|
+
path = folder / name
|
|
95
|
+
if not path.is_file() or path.stat().st_size < 64:
|
|
96
|
+
raise RuntimeError('Render target export failed: ' + name)
|
|
97
|
+
return file_record(name)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
assert request['schema'] == 'gripforge.unreal-export-request.v1'
|
|
102
|
+
assert Path(unreal.Paths.get_project_file_path()).resolve() == Path(request['project_file']).resolve()
|
|
103
|
+
source_file = Path(request['project_file']).parent / 'Content' / (request['source_level'][6:] + '.umap')
|
|
104
|
+
assert sha(source_file) == request['source_sha256'], 'Source level changed; prepare a new job'
|
|
105
|
+
progress('loading_source')
|
|
106
|
+
world = unreal.EditorLoadingAndSavingUtils.load_map(request['source_level'])
|
|
107
|
+
assert world and world.get_path_name().split('.')[0] == request['source_level']
|
|
108
|
+
actors = unreal.GameplayStatics.get_all_actors_of_class(world, unreal.Actor)
|
|
109
|
+
previous = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
|
|
110
|
+
if previous:
|
|
111
|
+
assert previous['request_hash'] == fingerprint, 'Job inputs changed'
|
|
112
|
+
manifest = dict(schema='gripforge.unreal-export.v1', request_hash=fingerprint,
|
|
113
|
+
source_level=request['source_level'], source_sha256=request['source_sha256'],
|
|
114
|
+
project=Path(request['project_file']).stem, units='metres', up='Y',
|
|
115
|
+
assets=previous.get('assets', {}), instances=[], terrains=previous.get('terrains', []),
|
|
116
|
+
environment={}, lights=[], cameras=[], omitted={}, warnings=[])
|
|
117
|
+
definitions, landscapes, dependencies = {}, [], {}
|
|
118
|
+
|
|
119
|
+
def omit(kind):
|
|
120
|
+
manifest['omitted'][kind] = manifest['omitted'].get(kind, 0) + 1
|
|
121
|
+
|
|
122
|
+
def pin(path):
|
|
123
|
+
p = package_file(path)
|
|
124
|
+
if p and p.is_file():
|
|
125
|
+
info = p.stat()
|
|
126
|
+
dependencies[str(p)] = [info.st_size, info.st_mtime_ns]
|
|
127
|
+
|
|
128
|
+
pinned_materials = set()
|
|
129
|
+
def pin_material(material):
|
|
130
|
+
if not material or material.get_path_name() in pinned_materials:
|
|
131
|
+
return
|
|
132
|
+
pinned_materials.add(material.get_path_name())
|
|
133
|
+
pin(material.get_path_name())
|
|
134
|
+
if isinstance(material, unreal.Material):
|
|
135
|
+
for tex in unreal.MaterialEditingLibrary.get_used_textures(material):
|
|
136
|
+
pin(tex.get_path_name())
|
|
137
|
+
else:
|
|
138
|
+
for value in native_prop(material, ['texture_parameter_values'], []):
|
|
139
|
+
tex = value.get_editor_property('parameter_value')
|
|
140
|
+
if tex:
|
|
141
|
+
pin(tex.get_path_name())
|
|
142
|
+
pin_material(native_prop(material, ['parent']))
|
|
143
|
+
|
|
144
|
+
for actor in actors:
|
|
145
|
+
cancelled()
|
|
146
|
+
cls = actor.get_class().get_path_name()
|
|
147
|
+
if isinstance(actor, unreal.LandscapeProxy):
|
|
148
|
+
landscapes.append(actor)
|
|
149
|
+
continue
|
|
150
|
+
if isinstance(actor, unreal.DirectionalLight):
|
|
151
|
+
comp = actor.get_component_by_class(unreal.DirectionalLightComponent)
|
|
152
|
+
direction = actor.get_actor_forward_vector()
|
|
153
|
+
color = comp.get_light_color()
|
|
154
|
+
manifest['environment'] = dict(sun=[-direction.x, -direction.z, -direction.y],
|
|
155
|
+
color=[color.r, color.g, color.b], intensity=comp.get_editor_property('intensity'))
|
|
156
|
+
continue
|
|
157
|
+
if isinstance(actor, unreal.CameraActor):
|
|
158
|
+
c = actor.get_camera_component()
|
|
159
|
+
p, d = actor.get_actor_location(), actor.get_actor_forward_vector()
|
|
160
|
+
manifest['cameras'].append(dict(name=actor.get_actor_label(), position=[p.x*.01,p.z*.01,p.y*.01],
|
|
161
|
+
target=[(p.x+d.x*1000)*.01,(p.z+d.z*1000)*.01,(p.y+d.y*1000)*.01], fov=c.field_of_view))
|
|
162
|
+
# Procedural UE skies and water are reported, not exported as opaque domes/planes.
|
|
163
|
+
if re.search(r'(?:GoodSky|UltraDynamicSky|SkySphere|SkyAtmosphere|CartoonWater)', cls, re.I):
|
|
164
|
+
omit(cls)
|
|
165
|
+
continue
|
|
166
|
+
components = actor.get_components_by_class(unreal.StaticMeshComponent)
|
|
167
|
+
supported = 0
|
|
168
|
+
for component in components:
|
|
169
|
+
mesh = component.static_mesh
|
|
170
|
+
if not mesh or not component.is_visible():
|
|
171
|
+
continue
|
|
172
|
+
if isinstance(component, unreal.SplineMeshComponent):
|
|
173
|
+
omit('SplineMeshComponent (deformed mesh requires a dedicated bake)')
|
|
174
|
+
continue
|
|
175
|
+
materials = [component.get_material(i) for i in range(component.get_num_materials())]
|
|
176
|
+
paths = [m.get_path_name() if m else None for m in materials]
|
|
177
|
+
source = dict(mesh=mesh.get_path_name(), materials=paths)
|
|
178
|
+
key = hashlib.sha256(json.dumps(source, sort_keys=True).encode()).hexdigest()[:24]
|
|
179
|
+
definitions[key] = (mesh, materials, source)
|
|
180
|
+
pin(mesh.get_path_name())
|
|
181
|
+
for material in materials:
|
|
182
|
+
pin_material(material)
|
|
183
|
+
transforms = []
|
|
184
|
+
if isinstance(component, unreal.InstancedStaticMeshComponent):
|
|
185
|
+
transforms = [component.get_instance_transform(i, world_space=True) for i in range(component.get_instance_count())]
|
|
186
|
+
else:
|
|
187
|
+
transforms = [component.get_world_transform()]
|
|
188
|
+
for i, t in enumerate(transforms):
|
|
189
|
+
identifier = component.get_path_name() + ':' + str(i)
|
|
190
|
+
manifest['instances'].append(dict(id='ue_'+hashlib.sha256(identifier.encode()).hexdigest()[:24],
|
|
191
|
+
name=actor.get_actor_label()[:145], source=identifier, asset=key, transform=transform(t),
|
|
192
|
+
batch=isinstance(component, unreal.InstancedStaticMeshComponent)))
|
|
193
|
+
supported += len(transforms)
|
|
194
|
+
if not supported and not components and any(s in cls for s in ['Niagara', 'Decal', 'SpotLight', 'RectLight', 'PointLight']):
|
|
195
|
+
omit(cls)
|
|
196
|
+
manifest['source_snapshot'] = dependencies
|
|
197
|
+
if previous.get('source_snapshot') and previous['source_snapshot'] != dependencies:
|
|
198
|
+
raise RuntimeError('Source dependencies changed; use a new export job')
|
|
199
|
+
atomic(manifest_path, manifest)
|
|
200
|
+
assert len(manifest['instances']) <= 100000, 'Map exceeds 100,000 portable instances; export regions'
|
|
201
|
+
progress('inventory', instances=len(manifest['instances']), unique_assets=len(definitions), landscapes=len(landscapes))
|
|
202
|
+
|
|
203
|
+
# UE finishes compiling before render-target capture/material baking.
|
|
204
|
+
# Commandlets do not tick the editor to apply asynchronous shader results.
|
|
205
|
+
# Preload the export shader as well, before the native blocking compile command.
|
|
206
|
+
unreal.load_asset('/Engine/EditorLandscapeResources/Landscape_Heightmap_To_RenderTarget2D')
|
|
207
|
+
unreal.load_asset('/Engine/EditorLandscapeResources/Landscape_Weightmap_To_RenderTarget2D')
|
|
208
|
+
unreal.SystemLibrary.execute_console_command(world, 'Editor.AsyncAssetCompilationFinishAll')
|
|
209
|
+
for landscape in landscapes:
|
|
210
|
+
cancelled()
|
|
211
|
+
key = landscape.get_name()
|
|
212
|
+
old = next((t for t in manifest['terrains'] if t['id'] == key), None)
|
|
213
|
+
if old and old.get('bake_version') == 2 and all(done(old[k]) for k in ['heightmap', 'albedo']):
|
|
214
|
+
continue
|
|
215
|
+
progress('terrain', terrain=key)
|
|
216
|
+
rotation = landscape.get_actor_rotation()
|
|
217
|
+
assert abs(rotation.pitch)+abs(rotation.yaw)+abs(rotation.roll) < .001, 'Rotated Landscape needs a dedicated adapter'
|
|
218
|
+
components = landscape.get_components_by_class(unreal.LandscapeComponent)
|
|
219
|
+
bases = [(int(c.get_editor_property('section_base_x')), int(c.get_editor_property('section_base_y'))) for c in components]
|
|
220
|
+
q = native_prop(components[0], ['ComponentSizeQuads', 'component_size_quads'])
|
|
221
|
+
if not q:
|
|
222
|
+
diffs = [abs(b-a) for axis in [0,1] for a,b in zip(sorted(set(p[axis] for p in bases)), sorted(set(p[axis] for p in bases))[1:])]
|
|
223
|
+
assert diffs, 'Cannot determine single-component Landscape resolution'
|
|
224
|
+
q = math.gcd(*diffs)
|
|
225
|
+
minx, miny = min(p[0] for p in bases), min(p[1] for p in bases)
|
|
226
|
+
maxx, maxy = max(p[0] for p in bases)+q, max(p[1] for p in bases)+q
|
|
227
|
+
width, height = maxx-minx+1, maxy-miny+1
|
|
228
|
+
assert max(width,height) <= 8192, 'Landscape exceeds 8192 height samples; export regions'
|
|
229
|
+
rt = unreal.RenderingLibrary.create_render_target2d(world, width, height, unreal.TextureRenderTargetFormat.RTF_RGBA8)
|
|
230
|
+
assert landscape.landscape_export_heightmap_to_render_target(rt, True, False)
|
|
231
|
+
heightmap = png(rt, key+'-height-rg.png')
|
|
232
|
+
unreal.RenderingLibrary.release_render_target2d(rt)
|
|
233
|
+
visibility = None
|
|
234
|
+
layer_names = [str(n) for n in landscape.get_target_layer_names(True)] if hasattr(landscape, 'get_target_layer_names') else []
|
|
235
|
+
visibility_name = next((n for n in layer_names if 'visibility' in n.lower()), None)
|
|
236
|
+
if visibility_name:
|
|
237
|
+
rt = unreal.RenderingLibrary.create_render_target2d(world, width, height, unreal.TextureRenderTargetFormat.RTF_RGBA8)
|
|
238
|
+
assert landscape.landscape_export_weightmap_to_render_target(rt, visibility_name), 'Visibility mask export failed'
|
|
239
|
+
visibility = png(rt, key+'-visibility.png')
|
|
240
|
+
unreal.RenderingLibrary.release_render_target2d(rt)
|
|
241
|
+
p, s = landscape.get_actor_location(), landscape.get_actor_scale3d()
|
|
242
|
+
assert s.x > 0 and s.y > 0 and s.z > 0, 'Reflected Landscape requires a dedicated adapter'
|
|
243
|
+
center = unreal.Vector(p.x+(minx+maxx)*s.x/2, p.y+(miny+maxy)*s.y/2, p.z+200000)
|
|
244
|
+
capture = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.SceneCapture2D, center, unreal.Rotator(-90,-90,0))
|
|
245
|
+
comp = capture.get_component_by_class(unreal.SceneCaptureComponent2D)
|
|
246
|
+
size = request.get('terrain_texture_size', 4096)
|
|
247
|
+
rt = unreal.RenderingLibrary.create_render_target2d(world, size, size, unreal.TextureRenderTargetFormat.RTF_RGBA8)
|
|
248
|
+
comp.set_editor_property('texture_target', rt)
|
|
249
|
+
comp.set_editor_property('projection_type', unreal.CameraProjectionMode.ORTHOGRAPHIC)
|
|
250
|
+
# Export a square orthographic projection; the manifest retains the crop.
|
|
251
|
+
span = max((maxx-minx)*s.x, (maxy-miny)*s.y)
|
|
252
|
+
comp.set_editor_property('ortho_width', span)
|
|
253
|
+
comp.set_editor_property('capture_source', unreal.SceneCaptureSource.SCS_BASE_COLOR)
|
|
254
|
+
comp.set_editor_property('primitive_render_mode', unreal.SceneCapturePrimitiveRenderMode.PRM_USE_SHOW_ONLY_LIST)
|
|
255
|
+
comp.show_only_actor_components(landscape)
|
|
256
|
+
comp.set_editor_property('capture_every_frame', False)
|
|
257
|
+
comp.set_editor_property('capture_on_movement', False)
|
|
258
|
+
comp.capture_scene()
|
|
259
|
+
albedo = png(rt, key+'-albedo.png')
|
|
260
|
+
unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(capture)
|
|
261
|
+
unreal.RenderingLibrary.release_render_target2d(rt)
|
|
262
|
+
row = dict(id=key, bake_version=2, heightmap=heightmap, albedo=albedo, width=width, height=height,
|
|
263
|
+
base=[minx,miny], components=bases, component_quads=q, origin=xyz(p), scale=xyz(s),
|
|
264
|
+
albedo_span_cm=span, material=landscape.get_editor_property('landscape_material').get_path_name())
|
|
265
|
+
if visibility:
|
|
266
|
+
row['visibility'] = visibility
|
|
267
|
+
manifest['terrains'] = [t for t in manifest['terrains'] if t['id'] != key]+[row]
|
|
268
|
+
atomic(manifest_path, manifest)
|
|
269
|
+
|
|
270
|
+
options = unreal.GLTFExportOptions()
|
|
271
|
+
options.set_editor_property('export_uniform_scale', .01)
|
|
272
|
+
options.set_editor_property('bake_material_inputs', unreal.GLTFMaterialBakeMode.USE_MESH_DATA)
|
|
273
|
+
options.set_editor_property('default_material_bake_size', unreal.GLTFMaterialBakeSize(request.get('texture_size',1024), request.get('texture_size',1024)))
|
|
274
|
+
options.set_editor_property('texture_image_format', unreal.GLTFTextureImageFormat.PNG)
|
|
275
|
+
options.set_editor_property('adjust_normalmaps', True)
|
|
276
|
+
options.set_editor_property('export_vertex_colors', False)
|
|
277
|
+
options.set_editor_property('export_lights', False)
|
|
278
|
+
options.set_editor_property('export_cameras', False)
|
|
279
|
+
options.set_editor_property('export_level_sequences', False)
|
|
280
|
+
(folder/'assets').mkdir(exist_ok=True)
|
|
281
|
+
for index, (key, (mesh, materials, source)) in enumerate(definitions.items()):
|
|
282
|
+
progress('meshes', completed=index, total=len(definitions), asset=mesh.get_name())
|
|
283
|
+
old = manifest['assets'].get(key)
|
|
284
|
+
if old and done(old):
|
|
285
|
+
continue
|
|
286
|
+
# Export one neutral instance of each mesh/material combination. No mutation of Library/native assets.
|
|
287
|
+
temp = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.StaticMeshActor, unreal.Vector(0,0,0))
|
|
288
|
+
comp = temp.static_mesh_component
|
|
289
|
+
comp.set_static_mesh(mesh)
|
|
290
|
+
for slot, material in enumerate(materials):
|
|
291
|
+
if material:
|
|
292
|
+
comp.set_material(slot, material)
|
|
293
|
+
name = 'assets/'+key+'.glb'
|
|
294
|
+
try:
|
|
295
|
+
unreal.GLTFExporter.export_to_gltf(world, str(folder/name), options, {temp})
|
|
296
|
+
assert (folder/name).is_file(), 'GLB export failed: '+source['mesh']
|
|
297
|
+
manifest['assets'][key] = dict(file_record(name), name=mesh.get_name(), source=source)
|
|
298
|
+
atomic(manifest_path, manifest)
|
|
299
|
+
finally:
|
|
300
|
+
unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(temp)
|
|
301
|
+
if manifest['terrains']:
|
|
302
|
+
progress('native_reference')
|
|
303
|
+
terrain = manifest['terrains'][0]
|
|
304
|
+
o, s, base = terrain['origin'], terrain['scale'], terrain['base']
|
|
305
|
+
sx, sy = (terrain['width']-1)*s[0], (terrain['height']-1)*s[1]
|
|
306
|
+
span = max(sx, sy)
|
|
307
|
+
target = unreal.Vector(o[0]+(base[0]+(terrain['width']-1)/2)*s[0], o[1]+(base[1]+(terrain['height']-1)/2)*s[1], o[2])
|
|
308
|
+
position = target + unreal.Vector(span*.55, span*.65, span*.7)
|
|
309
|
+
capture = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.SceneCapture2D, position, unreal.MathLibrary.find_look_at_rotation(position,target))
|
|
310
|
+
try:
|
|
311
|
+
comp = capture.get_component_by_class(unreal.SceneCaptureComponent2D)
|
|
312
|
+
rt = unreal.RenderingLibrary.create_render_target2d(world, 1600, 900, unreal.TextureRenderTargetFormat.RTF_RGBA8)
|
|
313
|
+
comp.set_editor_property('texture_target', rt)
|
|
314
|
+
comp.set_editor_property('fov_angle', math.degrees(2*math.atan(math.tan(math.radians(55)/2)*1600/900)))
|
|
315
|
+
comp.set_editor_property('capture_source', unreal.SceneCaptureSource.SCS_FINAL_COLOR_LDR)
|
|
316
|
+
comp.set_editor_property('capture_every_frame', False)
|
|
317
|
+
comp.set_editor_property('capture_on_movement', False)
|
|
318
|
+
unreal.SystemLibrary.execute_console_command(world, 'Editor.AsyncAssetCompilationFinishAll')
|
|
319
|
+
comp.capture_scene()
|
|
320
|
+
manifest['native_reference'] = png(rt, 'unreal-overview.png')
|
|
321
|
+
unreal.RenderingLibrary.release_render_target2d(rt)
|
|
322
|
+
finally:
|
|
323
|
+
unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(capture)
|
|
324
|
+
for file, before in dependencies.items():
|
|
325
|
+
info = Path(file).stat()
|
|
326
|
+
assert [info.st_size,info.st_mtime_ns] == before, 'Source changed during export: '+file
|
|
327
|
+
assert sha(source_file) == request['source_sha256'], 'Source level changed during export'
|
|
328
|
+
manifest['warnings'] = [
|
|
329
|
+
'Native Blueprint logic, Niagara, decals, animated water and procedural sky are not portable; omitted classes are counted.',
|
|
330
|
+
'Complex static materials are baked. Time/view/world-dependent shader behavior is not retained.',
|
|
331
|
+
'Landscape base color is a top-down native bake; material paint layers remain a source provenance, not an editable UE shader graph.',
|
|
332
|
+
]
|
|
333
|
+
manifest['status'] = 'exported-awaiting-gripforge-review'
|
|
334
|
+
atomic(manifest_path,manifest)
|
|
335
|
+
atomic(state_path,dict(status='succeeded',stage='exported',request_hash=fingerprint,updated_at=time.time(),
|
|
336
|
+
instances=len(manifest['instances']),assets=len(manifest['assets']),terrains=len(manifest['terrains']),lifecycle='work'))
|
|
337
|
+
except Exception as error:
|
|
338
|
+
atomic(state_path, dict(status='cancelled' if isinstance(error,InterruptedError) else 'failed',
|
|
339
|
+
error=str(error), traceback=traceback.format_exc(), request_hash=fingerprint,updated_at=time.time()))
|
|
340
|
+
unreal.log_error(traceback.format_exc())
|
|
341
|
+
raise
|