mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add spatial irradiance and character lighting adapter
This commit is contained in:
1 parent
8c31384a12
commit
7d44d84f19
15 files changed
+730
-5
No files matched your search
@@ -0,0 +1,55 @@
|
||||
"""Export authored character shader parameters and default SH coefficients."""
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from PIL import Image
|
||||
from pathlib import Path
|
||||
from .environment import Collections, key_hash
|
||||
|
||||
|
||||
def convert(models, private, converted):
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'vendor/utt'))
|
||||
import mdl_parser
|
||||
collections = Collections(converted)
|
||||
result = {'materials': {}}
|
||||
recipe = json.loads((Path(__file__).resolve().parents[1] / 'default_skater_retail_manifest.json').read_text())
|
||||
components = {c['slot']: c for c in recipe['components']}
|
||||
for slot in sorted(models.iterdir()):
|
||||
if not slot.is_dir():
|
||||
continue
|
||||
files = list(slot.glob('*.rx2'))
|
||||
if len(files) != 1:
|
||||
raise ValueError('Ambiguous character lighting model: '+slot.name)
|
||||
model = mdl_parser.parse_rx2(files[0].read_bytes())
|
||||
shaders = {p.value for p in model.materials if p.kind == 'AttribulatorMaterialName'}
|
||||
if len(shaders) != 1:
|
||||
raise ValueError('Ambiguous character shader: '+slot.name)
|
||||
shader = shaders.pop()
|
||||
family, key = shader.split('.', 1)
|
||||
fields, _ = collections.resolve('material_'+family, key)
|
||||
rows = fields[key_hash('m_params')]['array']['items']
|
||||
result['materials']['Retail_'+slot.name] = dict(shader=shader,
|
||||
params=[struct.unpack('>4f', bytes.fromhex(row)) for row in rows])
|
||||
specular = components[slot.name]['textures'].get('specular')
|
||||
if specular:
|
||||
# The native skin shader consumes R, not luminance or roughness.
|
||||
source = private / 'default_skater/textures/decoded' / (specular+'.png')
|
||||
destination = private / 'native-character' / (slot.name+'_specular.png')
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Image.open(source) as image:
|
||||
image.convert('RGB').getchannel('R').save(destination)
|
||||
result['materials']['Retail_'+slot.name]['specular'] = 'private/native-character/'+destination.name
|
||||
fields, _ = collections.resolve('Hash_6A2B5946514B8490', 'freeskate')
|
||||
result['default_sh'] = [struct.unpack('>3f', bytes.fromhex(row))
|
||||
for row in fields[key_hash('Hash_F45DFCAB6EC195CD')]['array']['items']]
|
||||
(private/'character-lighting.json').write_text(json.dumps(result))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--models', type=Path, required=True)
|
||||
p.add_argument('--private', type=Path, required=True)
|
||||
p.add_argument('--collections', type=Path, required=True)
|
||||
a = p.parse_args()
|
||||
convert(a.models, a.private, json.loads(a.collections.read_text()))
|
||||
@@ -188,6 +188,8 @@ def install(iso,base,game_exe,report,game_root=None):
|
||||
report('Building the skater model and rig')
|
||||
from .character_glb import convert as write_character
|
||||
write_character(character/'selected/models',private,manifest)
|
||||
from .character_lighting import convert as write_character_lighting
|
||||
write_character_lighting(character/'selected/models',private,converted)
|
||||
game_manifest={'version':1,'character_scene':'private/skater.glb','initial_animation':'R_IDLE_HCOM_000',
|
||||
'action_graph':'private/stock/data/state/ActionGraph_OnBoard.stategraph',
|
||||
'motion_graph':'private/stock/data/state/MotionGraph_OnBoard.stategraph'}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Export retail RWOBJECTTYPE_IRRADIANCEDATA (0xEB0024) without resampling."""
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def decode(raw):
|
||||
if raw[:7] != b'\x89RW4xb2':
|
||||
raise ValueError('Not an Xbox RX2 resource')
|
||||
count, table = struct.unpack_from('>I', raw, 32)[0], struct.unpack_from('>I', raw, 48)[0]
|
||||
groups = []
|
||||
for i in range(count):
|
||||
offset, _, size, _, _, kind = struct.unpack_from('>6I', raw, table + i*24)
|
||||
if kind != 0xEB0024:
|
||||
continue
|
||||
n, start = struct.unpack_from('>2I', raw, offset)
|
||||
if start < 8 or start+n*160 > size or offset+size > len(raw):
|
||||
raise ValueError('Invalid irradiance record extent')
|
||||
records = []
|
||||
for j in range(n):
|
||||
values = struct.unpack_from('>39fI', raw, offset+start+j*160)
|
||||
if not all(math.isfinite(x) for x in values[:39]):
|
||||
raise ValueError('Non-finite irradiance sample')
|
||||
records.append(struct.pack('<39fI', *values))
|
||||
if records:
|
||||
groups.append(records)
|
||||
return groups
|
||||
|
||||
|
||||
def write(manifest_path, output):
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
groups = []
|
||||
for asset in manifest['other_presentation_assets']:
|
||||
groups.extend(decode((manifest_path.parent / asset['rx2']).read_bytes()))
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output.open('wb') as f:
|
||||
f.write(b'SHP1' + struct.pack('<I', len(groups)))
|
||||
for records in groups:
|
||||
f.write(struct.pack('<I', len(records)))
|
||||
f.write(b''.join(records))
|
||||
return sum(map(len, groups))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--manifest', type=Path, required=True)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print('Exported', write(args.manifest, args.output), 'irradiance samples')
|
||||
@@ -151,6 +151,9 @@ def write(manifest_path,output,collision,report=lambda _:None, *, render_only=Fa
|
||||
for tag,data in extensions:
|
||||
f.write(tag);u(f,1,len(data));stored(f,data)
|
||||
report('Map written: '+m['map_name'])
|
||||
if not render_only:
|
||||
from tools.asset_pipeline.irradiance import write as write_irradiance
|
||||
write_irradiance(manifest_path, output.with_suffix('.irradiance'))
|
||||
|
||||
if __name__=='__main__':
|
||||
import argparse
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""RX2 irradiance layout checks without owned-game fixtures."""
|
||||
import struct
|
||||
import unittest
|
||||
|
||||
from .irradiance import decode
|
||||
|
||||
|
||||
def resource():
|
||||
data = bytearray(272)
|
||||
data[:7] = b'\x89RW4xb2'
|
||||
struct.pack_into('>I', data, 32, 1)
|
||||
struct.pack_into('>I', data, 48, 64)
|
||||
struct.pack_into('>6I', data, 64, 96, 0, 176, 0, 0, 0xEB0024)
|
||||
struct.pack_into('>2I', data, 96, 1, 16)
|
||||
values = [i / 4 for i in range(39)]
|
||||
struct.pack_into('>39fI', data, 112, *values, 0xDEDEDEDE)
|
||||
return data, values
|
||||
|
||||
|
||||
class IrradianceTests(unittest.TestCase):
|
||||
def test_preserves_coefficients_position_and_link_word(self):
|
||||
data, values = resource()
|
||||
groups = decode(data)
|
||||
self.assertEqual(len(groups), 1)
|
||||
self.assertEqual(struct.unpack('<39fI', groups[0][0]), (*values, 0xDEDEDEDE))
|
||||
|
||||
def test_rejects_records_outside_section(self):
|
||||
data, _ = resource()
|
||||
struct.pack_into('>I', data, 96, 2)
|
||||
with self.assertRaises(ValueError):
|
||||
decode(data)
|
||||
|
||||
def test_rejects_nonfinite_sh(self):
|
||||
data, _ = resource()
|
||||
struct.pack_into('>f', data, 112, float('nan'))
|
||||
with self.assertRaises(ValueError):
|
||||
decode(data)
|
||||
Reference in new issue
Block a user