ikaruga/NinjaModel.py
Benjamin Collins bf94f20bac normals
2020-08-24 22:58:21 +09:00

732 lines
17 KiB
Python

"""
MIT License
Copyright (c) 2020 Benjamin Collins (kion @ dashgl.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import os
import sys
import struct
from PowerVR import PowerVR
from NinjaTexture import NinjaTexture
from NinjaMaterial import NinjaMaterial
from NinjaVertex import NinjaVertex
from NinjaFace import NinjaFace
from NinjaBone import NinjaBone
from NinjaMotion import NinjaMotion
class NinjaModel:
def __init__(self, model_name, tex_name = ''):
self.debug = False
self.hasMesh = []
# File Information
self.model_name = model_name
self.model_path = 'input/' + model_name
self.file = open(self.model_path, 'rb')
self.length = os.path.getsize(self.model_path)
self.tex_name = tex_name
self.tex_path = 'input/' + tex_name
# Textures
self.mat_list = []
self.tex_list = []
self.anim_list = []
# Bone List
self.bones = []
#Vertex List
self.index_lookup = []
self.vertex_list = []
self.face_list = []
self.strip_count = 0;
return None
def parse(self):
self.parseTexture()
self.parseModel()
return None
def parseTexture(self):
pvm = PowerVR(self.tex_name)
self.tex_list = pvm.parse()
return None
def parseModel(self):
while self.file.tell() < self.length:
bytes = self.file.read(4)
if(bytes == b'NJTL') :
bytes = self.file.read(4)
length = struct.unpack('I', bytes)[0]
pos = self.file.tell() + length
self.readNjtl()
self.file.seek(pos, 0)
elif (bytes == b'NJCM') :
bytes = self.file.read(4)
length = struct.unpack('I', bytes)[0]
pos = self.file.tell() + length
self.pof = self.file.tell()
self.readNjcm(None)
self.file.seek(pos, 0)
elif (bytes == b'NMDM') :
bytes = self.file.read(4)
length = struct.unpack('I', bytes)[0]
pos = self.file.tell() + length
self.pof = self.file.tell()
self.readNmdm()
self.file.seek(pos, 0)
print(self.hasMesh)
self.file.close()
return None
def readNjtl(self):
pof = self.file.tell()
bytes = self.file.read(8)
n = struct.unpack('II', bytes)
str_ofs = []
self.file.seek(n[0] + pof, 0)
for i in range(0, n[1]):
bytes = self.file.read(12)
m = struct.unpack('III', bytes)
str_ofs.append(m[0] + pof)
for ofs in str_ofs:
name = ''
self.file.seek(ofs, 0)
while 1:
ch = self.file.read(1)
if(ch == b'\0'):
break
name += ch.decode()
return None
def readNjcm(self, parentBone):
bytes = self.file.read(52)
b = struct.unpack('IIfffiiifffII', bytes)
flags = b[0]
model_ofs = b[1]
c = 2 * 3.141592 / 0xFFFF;
pos = ( b[2], b[3], b[4] )
rot = ( b[5] * c, b[6] * c, b[7] *c)
scl = ( b[8], b[9], b[10] )
child_ofs = b[11]
sibling_ofs = b[12]
self.bone = NinjaBone()
num = len(self.bones)
self.bone.setName(num)
self.bones.append(self.bone)
if self.debug:
print("Reading bone: %s" % self.bone.name)
if ( (flags & 0x04) == 0):
self.bone.setScale(scl)
if self.debug:
print("Setting Scale: %s", scl)
if ( (flags & 0x02) == 0):
zxy_set = flags & 0x20
self.bone.setRotation(rot)
if self.debug:
print("Setting Rotation: ", rot)
if ( (flags & 0x01) == 0):
self.bone.setPosition(pos)
if self.debug:
print("Setting Position: ", pos)
if parentBone:
parentBone.add(self.bone)
if model_ofs:
self.hasMesh.append(self.bone.index)
if self.debug:
print("Reading Mesh @ 0x%08x" % self.file.tell())
self.file.seek(model_ofs + self.pof, 0)
self.readModel()
if child_ofs:
self.file.seek(child_ofs + self.pof, 0)
self.readNjcm(self.bone)
if sibling_ofs:
self.file.seek(sibling_ofs + self.pof, 0)
self.readNjcm(parentBone)
return None
def readModel(self):
bytes = self.file.read(24)
m = struct.unpack('IIffff', bytes)
vertex_ofs = m[0]
chunk_ofs = m[1]
center = (m[2], m[3], m[4])
radius = m[5]
if vertex_ofs:
self.file.seek(vertex_ofs + self.pof, 0)
self.readVertexList()
if chunk_ofs:
self.file.seek(chunk_ofs + self.pof, 0)
self.readChunkList()
return None
def readVertexList(self):
bytes = self.file.read(8)
c = struct.unpack('BBHHH', bytes)
chunk_head = c[0]
chunk_flag = c[1]
chunk_len = c[2]
vertex_ofs = c[3]
vertex_count = c[4]
readColor = False
readNormal = False
if chunk_head == 0x23:
readColor = True
elif chunk_head == 0x29:
readNormal = True
else:
print("ERROR!!! NEW VERTEX TYPE!!!!")
print("New Vertex Type: 0x%02x" % chunk_head)
if self.debug:
print("--- Reading vertex list ---")
print("Vertex Count: %d" % vertex_count)
print("Vertex Offset: %d" % vertex_ofs)
for i in range(vertex_count):
vertex = NinjaVertex()
# Position
bytes = self.file.read(12)
v = struct.unpack('fff', bytes)
pos = [ v[0], v[1], v[2] ]
pos = self.bone.apply(pos)
vertex.setPosition( pos[0], pos[1], pos[2] )
# Normal
if readNormal:
bytes = self.file.read(12)
v = struct.unpack('fff', bytes)
norm = [ v[0], v[1], v[2] ]
norm = self.bone.applyNorm(norm)
vertex.setNormal( norm[0], norm[1], norm[2] )
# Vertex Color
if readColor:
bytes = self.file.read(4)
v = struct.unpack('BBBB', bytes)
r = v[0]
b = v[1]
g = v[2]
a = v[3]
vertex.setColor(r, g, b, a)
vertex.setSkinWeight(0, self.bone.index, 1.0)
while len(self.index_lookup) < vertex_ofs + 1:
self.index_lookup.append(None)
self.index_lookup[vertex_ofs] = len(self.vertex_list)
vertex_ofs += 1
self.vertex_list.append(vertex)
pos = self.file.tell()
bytes = self.file.read(2)
c = struct.unpack('BB', bytes)
chunk_head = c[0]
chunk_flag = c[1]
if chunk_head != 255:
print("ERROR ANOTHER VERTEX LIST DETECTED")
return None
def readChunkList(self):
if self.debug:
print("--- Reading Chunks @ 0x%08x ---" % self.file.tell())
while 1:
bytes = self.file.read(2)
c = struct.unpack('BB', bytes)
chunk_head = c[0]
chunk_flag = c[1]
if chunk_head == 255:
if self.debug:
print("End Chunk Found")
break
elif chunk_head == 0:
if self.debug:
print("Null Chunk Found")
continue
elif chunk_head >= 1 and chunk_head <= 5:
if self.debug:
print("Bits Chunk Found")
if chunk_head == 1:
print("Blend Alpha Adjust!!")
elif chunk_head == 2:
print("Mipmap adjust")
elif chunk_head == 3:
print("Specular exponent")
elif chunk_head == 4:
print("Save offset!!!")
elif chunk_head == 5:
print("Jumpt to offset!!!")
elif chunk_head >= 17 and chunk_head <= 23:
if self.debug:
print("Material Chunk Found @ 0x%08x" % self.file.tell())
bytes = self.file.read(2)
short_len = struct.unpack('H', bytes)
self.material = NinjaMaterial()
dst_alpha = chunk_flag & 0x07
src_alpha = chunk_flag >> 3
if chunk_head & 0x01:
bytes = self.file.read(4)
c = struct.unpack('BBBB', bytes)
b = c[0] / 255.0
g = c[1] / 255.0
r = c[2] / 255.0
a = c[3] / 255.0
self.material.setDiffuseColor(r, g, b, a)
if chunk_head & 0x02:
bytes = self.file.read(4)
c = struct.unpack('BBBB', bytes)
b = c[0] / 255.0,
g = c[1] / 255.0,
r = c[2] / 255.0,
a = c[3] / 255.0
if chunk_head & 0x04:
bytes = self.file.read(4)
c = struct.unpack('BBBB', bytes)
b = c[0] / 255.0,
g = c[1] / 255.0,
r = c[2] / 255.0,
coef = c[3] / 255.0
elif chunk_head >= 8 and chunk_head <= 9:
if self.debug:
print("Texture Chunk Found @ 0x%08x" % self.file.tell())
bytes = self.file.read(2)
chunk_body = struct.unpack('H', bytes)[0]
mip_depth = chunk_flag & 0x07
clamp_u = chunk_flag & 0x08
clamp_v = chunk_flag & 0x10
flip_u = chunk_flag & 0x20
flip_v = chunk_flag & 0x40
tex_id = chunk_body & 0x1fff
super_sample = chunk_body >> 13 & 0x01
filter_sample = chunk_body >> 14 & 0x03
self.material = self.material.clone()
self.material.setTexIndex(tex_id)
elif chunk_head >= 64 and chunk_head <= 66:
if self.debug:
print("Strip Chunk Found @ 0x%08x" % self.file.tell())
bytes = self.file.read(2)
h = struct.unpack('H', bytes)
chunk_len = h[0] * 2
snap_to = self.file.tell() + chunk_len
bytes = self.file.read(2)
h = struct.unpack('H', bytes)
chunk_body = h[0]
if self.debug:
print("Strip Length (bytes): 0x%04x" % chunk_len)
print("Expected End: 0x%08x" % (self.file.tell() + chunk_len))
mat_index = -1
for i in range(len(self.mat_list)):
if self.material != self.mat_list[i]:
continue
mat_index = i
break
if mat_index == -1:
mat_index = len(self.mat_list)
self.material.setIndex(len(self.mat_list))
self.mat_list.append(self.material)
ignore_light = chunk_flag & 0x01
ignore_specular = chunk_flag & 0x02
ignore_ambient = chunk_flag & 0x04
use_alpha = chunk_flag & 0x08
double_side = chunk_flag & 0x10
flat_shading = chunk_flag & 0x20
environment_mapping = chunk_flag & 0x20
strip_count = chunk_body & 0x3fff;
user_offset = chunk_body >> 14
if self.debug:
print("Strip Count: %d" % strip_count)
print("User Offset: %d" % user_offset)
for i in range (strip_count): # {
bytes = self.file.read(2)
strip_len = struct.unpack('h', bytes)[0]
if self.debug:
print("Strip length: %d" % strip_len)
clockwise = strip_len < 0
strip_len = abs(strip_len)
strip = []
indices = []
for k in range (strip_len): # {
bytes = self.file.read(2)
index = struct.unpack('H', bytes)[0]
indices.append(index)
index = self.index_lookup[index]
if chunk_head == 64:
strip.append({
'index' : index,
'uv' : { 'u' : 0, 'v' : 0 }
})
continue
bytes = self.file.read(4)
uv = struct.unpack('hh', bytes)
u = uv[0]
v = uv[1]
if chunk_head == 65:
u = u / 255.0
v = v / 255.0
elif chunk_head == 66:
u = u / 1023.0
v = v / 1023.0
strip.append({
'index' : index,
'uv' : { 'u' : u, 'v' : v }
})
for k in range(len(strip) - 2):
if ((clockwise and not (k % 2)) or ( not clockwise and k % 2)):
a = strip[k + 2]
b = strip[k + 1]
c = strip[k + 0]
else :
a = strip[k + 2]
b = strip[k + 0]
c = strip[k + 1]
ai = a['index']
bi = b['index']
ci = c['index']
va = self.vertex_list[ai]
vb = self.vertex_list[bi]
vc = self.vertex_list[ci]
face = NinjaFace()
face.setMatIndex(mat_index)
face.setIndexes(ai, bi, ci)
face.setNormals(va.norm, vb.norm, vc.norm)
face.setDiffuseUv(a['uv'], b['uv'], c['uv'])
self.face_list.append(face)
self.file.seek(snap_to, 0)
self.strip_count = self.strip_count + 1
else:
print("Unknown File Position: 0x%08x" % self.file.tell())
return None
def readNmdm(self):
if self.debug:
print("READING NINJA MOTION")
# Read Header
bytes = self.file.read(12)
p = struct.unpack('IIHH', bytes)
table_ofs = self.pof + p[0]
frame_count = p[1]
flags = p[2]
factor_count = p[3] & 0x03
spline_interpolation = p[3] & 0x40
user_function_interpolation = p[3] & 0x80
anim = NinjaMotion()
anim.setName(len(self.anim_list))
anim.setFrames(frame_count)
anim.setSkeleton(self.bones)
self.anim_list.append(anim)
if self.debug:
print("Frame Count: %d" % frame_count)
print("Flags: %x" % flags)
print("Animation Name: ", anim.name)
if flags & 0x01:
print("Has Position!")
if flags & 0x20:
print("Has Position!")
if flags & 0x01:
print("Has Position!")
# Read Motion Table
anim_vals = []
self.file.seek(table_ofs, 0)
for i in range(len(self.bones)):
entry = {
'bone_index' : i,
'pos_ofs' : 0,
'pos_num' : 0,
'rot_ofs' : 0,
'rot_num' : 0,
'scl_ofs' : 0,
'scl_num' : 0
}
if flags & 0x01:
bytes = self.file.read(4)
p = struct.unpack('I', bytes)
if(p[0]):
entry['pos_ofs'] = self.pof + p[0]
if flags & 0x20:
bytes = self.file.read(4)
p = struct.unpack('I', bytes)
if(p[0]):
entry['rot_ofs'] = self.pof + p[0]
if flags & 0x04:
bytes = self.file.read(4)
p = struct.unpack('I', bytes)
if(p[0]):
entry['scl_ofs'] = self.pof + p[0]
if flags & 0x01:
bytes = self.file.read(4)
p = struct.unpack('I', bytes)
entry['pos_num'] = p[0]
if flags & 0x20:
bytes = self.file.read(4)
p = struct.unpack('I', bytes)
entry['rot_num'] = p[0]
if flags & 0x04:
bytes = self.file.read(4)
p = struct.unpack('I', bytes)
entry['scl_num'] = p[0]
anim_vals.append(entry)
c = 2 * 3.141592 / 0x10000;
for entry in anim_vals:
boneIndex = entry['bone_index']
self.file.seek(entry['pos_ofs'], 0)
for i in range(entry['pos_num']):
bytes = self.file.read(16)
p = struct.unpack('Ifff', bytes)
frame = p[0]
vec3 = [ p[1], p[2], p[3] ]
anim.appendKeyFrame(boneIndex, frame, vec3, 'pos')
self.file.seek(entry['rot_ofs'], 0)
for i in range(entry['rot_num']):
bytes = self.file.read(8)
p = struct.unpack('Hhhh', bytes)
frame = p[0]
vec3 = [ p[1]*c, p[2]*c, p[3]*c ]
anim.appendKeyFrame(boneIndex, frame, vec3, 'rot')
self.file.seek(entry['scl_ofs'], 0)
for i in range(entry['scl_num']):
bytes = self.file.read(16)
p = struct.unpack('Ifff', bytes)
frame = p[0]
vec3 = [ p[1], p[2], p[3] ]
anim.appendKeyFrame(boneIndex, frame, vec3, 'scl')
anim.save(boneIndex)
return None
def exportObj(self):
pre, ext = os.path.splitext(self.model_name)
pre = pre.replace('/', '_')
f = open('output/' + pre + '.OBJ', 'w')
for vert in self.vertex_list:
f.write('v')
f.write(' %.03f' % vert.pos['x'])
f.write(' %.03f' % vert.pos['y'])
f.write(' %.03f' % vert.pos['z'])
f.write('\r\n')
f.write('\r\n')
for face in self.face_list:
f.write('f')
f.write(' %d' % (face.index[0] + 1))
f.write(' %d' % (face.index[1] + 1))
f.write(' %d' % (face.index[2] + 1))
f.write('\r\n')
f.write('\r\n')
f.close()
return None
def export(self):
pre, ext = os.path.splitext(self.model_name)
pre = pre.replace('/', '_')
f = open('output/' + pre + '.DMF', 'wb')
# Write Magic
f.write(b'DASH')
f.write(struct.pack('HHII', 1, 5, 0x10, 0x06))
#Write Table
f.write(b'tex\0')
f.write(struct.pack('III', 0, 0, 0))
f.write(b'mat\0')
f.write(struct.pack('III', 0, 0, 0))
f.write(b'vert')
f.write(struct.pack('III', 0, 0, 0))
f.write(b'face')
f.write(struct.pack('III', 0, 0, 0))
f.write(b'bone')
f.write(struct.pack('III', 0, 0, 0))
f.write(b'anim')
f.write(struct.pack('III', 0, 0, 0))
# Write Textures
pos = f.tell()
count = len(self.tex_list)
f.seek(0x18, 0)
f.write(struct.pack('II', pos, count))
f.seek(0, 2)
for tex in self.tex_list:
tex.createDmfEntry(f)
for tex in self.tex_list:
tex.writeDmfTexture(f)
# Write Materials
pos = f.tell()
count = len(self.mat_list)
f.seek(0x28, 0)
f.write(struct.pack('II', pos, count))
f.seek(0, 2)
for mat in self.mat_list:
mat.createDmfEntry(f)
# Write Vertices
pos = f.tell()
count = len(self.vertex_list)
f.seek(0x38, 0)
f.write(struct.pack('II', pos, count))
f.seek(0, 2)
for vert in self.vertex_list:
vert.createDmfEntry(f)
while f.tell() % 0x10:
f.write(struct.pack('B', 0))
# Write Faces
pos = f.tell()
count = len(self.face_list)
f.seek(0x48, 0)
f.write(struct.pack('II', pos, count))
f.seek(0, 2)
for face in self.face_list:
face.createDmfEntry(f)
while f.tell() % 0x10:
f.write(struct.pack('B', 0))
# Write Bones
pos = f.tell()
count = len(self.bones)
f.seek(0x58, 0)
f.write(struct.pack('II', pos, count))
f.seek(0, 2)
for bone in self.bones:
bone.createDmfEntry(f)
#Write Animations
pos = f.tell()
count = len(self.anim_list)
f.seek(0x68, 0)
f.write(struct.pack('II', pos, count))
f.seek(0, 2)
for anim in self.anim_list:
anim.generateDmfEntry(f)
for anim in self.anim_list:
anim.writeDmfAnimation(f)
f.close()
return None