272 lines
11 KiB
Python
272 lines
11 KiB
Python
# Copyright (C) 2022 RozK
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import sys
|
|
import struct
|
|
from array import array
|
|
from pathlib import Path
|
|
from itertools import starmap, chain, repeat
|
|
|
|
from engine import *
|
|
|
|
from game.math import float_s8, vec3_srgb8a8
|
|
from game.resources import load_png, TextureData, VerticesData, ModelData, Archive
|
|
|
|
_texture_flags = TEXTURE_FLAG_MIPMAPS | TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_NEAREST
|
|
|
|
class ObjArchive(Archive):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def import_hacks(self, path):
|
|
print("Importing hacks", path)
|
|
hacks = {}
|
|
for line in open(path):
|
|
data = line.split()
|
|
if not data:
|
|
continue
|
|
name = data.pop(0)
|
|
if name == '#':
|
|
continue
|
|
assert len(data) == 4 and data[0] == '=' and data[2] == 'mesh'
|
|
alias = data[1]
|
|
mesh = data[3]
|
|
assert name not in hacks.keys()
|
|
hacks[name] = (alias, mesh)
|
|
return hacks
|
|
|
|
def import_mtl(self, mtlpath):
|
|
def load_value(x):
|
|
x = round(float(x), 6)
|
|
if x == -0.0:
|
|
x = 0.0
|
|
return x
|
|
size = None
|
|
def load_texture(pngpath, nchannels):
|
|
width, height, pixels = load_png(pngpath)
|
|
assert pixels.typecode == 'B'
|
|
assert (size is None or size == (width, height))
|
|
assert len(pixels) == width * height * nchannels
|
|
return ((width, height), pixels)
|
|
print("Importing mtl", mtlpath)
|
|
texture = array('B')
|
|
texlevels = {}
|
|
name = ''
|
|
texlevel = 0
|
|
color = (0, 0, 0, 0)
|
|
metallic = 0
|
|
specular = 0
|
|
roughness = 0
|
|
bump = 0
|
|
def finish(_texture, _color, _metallic, _specular, _roughness, _bump):
|
|
width, height = size
|
|
if not isinstance(_color, array):
|
|
_color = array('B', _color * (width * height))
|
|
_texture.extend(_color)
|
|
if not isinstance(_metallic, array):
|
|
_metallic = repeat(_metallic, width * height)
|
|
if not isinstance(_specular, array):
|
|
_specular = repeat(_specular, width * height)
|
|
if not isinstance(_roughness, array):
|
|
_roughness = repeat(_roughness, width * height)
|
|
if not isinstance(_bump, array):
|
|
_bump = repeat(_bump, width * height)
|
|
_texture.extend(chain.from_iterable(zip(_metallic, _specular, _roughness, _bump)))
|
|
for line in open(mtlpath):
|
|
data = line.split()
|
|
if not data:
|
|
continue
|
|
code = data.pop(0)
|
|
if code == '#':
|
|
continue
|
|
elif code == 'newmtl':
|
|
if name:
|
|
if size is None:
|
|
size = (1, 1)
|
|
finish(texture, color, metallic, specular, roughness, bump)
|
|
texlevels[name] = texlevel
|
|
texlevel += 2
|
|
name = data[0]
|
|
print("Importing material", name)
|
|
color = (0, 0, 0, 0)
|
|
metallic = 0
|
|
specular = 0
|
|
roughness = 0
|
|
bump = 0
|
|
elif code == 'Kd': # color
|
|
color = vec3_srgb8a8(tuple(map(load_value, data)))
|
|
assert len(color) == 4
|
|
elif code == 'Ni': # ior -> metallic
|
|
metallic = float_s8(load_value(data[0]))
|
|
elif code == 'Ks': # specular
|
|
specular = float_s8(load_value(data[0]))
|
|
elif code == 'Ns': # roughness
|
|
roughness = float_s8(1.0 - load_value(data[0]) / 1000.0)
|
|
elif code == 'map_Kd':
|
|
pngpath = mtlpath.parent / data[0]
|
|
print("Importing color texture", pngpath)
|
|
size, color = load_texture(pngpath, 4)
|
|
pngpath = pngpath.parent / (pngpath.stem + '_b.png')
|
|
if pngpath.exists():
|
|
print("Importing bump channel", pngpath)
|
|
size, bump = load_texture(pngpath, 1)
|
|
elif code == 'map_Ni':
|
|
pngpath = mtlpath.parent / data[0]
|
|
print("Importing metallic channel", pngpath)
|
|
size, metallic = load_texture(pngpath, 1)
|
|
elif code == 'map_Ks':
|
|
pngpath = mtlpath.parent / data[0]
|
|
print("Importing specular channel", pngpath)
|
|
size, specular = load_texture(pngpath, 1)
|
|
elif code == 'map_Ns':
|
|
pngpath = mtlpath.parent / data[0]
|
|
print("Importing roughness channel", pngpath)
|
|
size, roughness = load_texture(pngpath, 1)
|
|
if name:
|
|
if size is None:
|
|
size = (1, 1)
|
|
finish(texture, color, metallic, specular, roughness, bump)
|
|
texlevels[name] = texlevel
|
|
texlevel += 2
|
|
name = str(mtlpath.stem)
|
|
assert texlevel < 255
|
|
assert name not in self.textures_db.keys()
|
|
print("Storing texture", name)
|
|
width, height = size
|
|
self.textures_db[name] = TextureData(
|
|
name, TEXTURE_FORMAT_SRGB8_A8, width, height, texlevel, _texture_flags, texture)
|
|
return texlevels
|
|
|
|
def import_obj(self, objpath):
|
|
def load_coord(x):
|
|
x = round(float(x), 6)
|
|
if x == -0.0:
|
|
x = 0.0
|
|
return x
|
|
def load_index(x):
|
|
return int(x) - 1
|
|
print("Importing obj", objpath)
|
|
texlevels = {}
|
|
name = ''
|
|
texlevel = 0
|
|
mesh = []
|
|
positions = []
|
|
normals = []
|
|
texcoords = []
|
|
objects = {}
|
|
for line in open(objpath):
|
|
data = line.split()
|
|
if not data:
|
|
continue
|
|
code = data.pop(0)
|
|
if code == '#':
|
|
continue
|
|
elif code == 'mtllib':
|
|
texlevels = self.import_mtl(objpath.parent / data[0])
|
|
elif code == 'o':
|
|
if name:
|
|
assert mesh
|
|
objects[name] = (texlevel, mesh)
|
|
name = data[0]
|
|
print("Importing object", name)
|
|
mesh = []
|
|
elif code == 'usemtl':
|
|
texlevel = texlevels[data[0]]
|
|
elif code == 'v':
|
|
position = tuple(map(load_coord, data))
|
|
assert len(position) == 3
|
|
positions.append(position)
|
|
elif code == 'vn':
|
|
normal = tuple(map(load_coord, data))
|
|
assert len(normal) == 3
|
|
normals.append(normal)
|
|
elif code == 'vt':
|
|
texcoord = tuple(map(load_coord, data))
|
|
assert len(texcoord) == 2
|
|
texcoords.append((texcoord[0], 1.0 - texcoord[1]))
|
|
elif code == 'f':
|
|
indices = tuple(map(lambda x: tuple(map(load_index, x.split('/'))), data))
|
|
assert len(indices) == 3
|
|
assert len(indices[0]) == 3
|
|
assert len(indices[1]) == 3
|
|
assert len(indices[2]) == 3
|
|
triangle = tuple(map(lambda x: positions[x[0]] + normals[x[2]] + texcoords[x[1]], indices))
|
|
assert len(triangle) == 3 and len(set(triangle)) == 3
|
|
mesh.append(triangle)
|
|
if name:
|
|
assert mesh
|
|
objects[name] = (texlevel, mesh)
|
|
vertices = set()
|
|
for _, mesh in objects.values():
|
|
vertices |= frozenset(chain.from_iterable(mesh))
|
|
vertices = tuple(vertices)
|
|
indices = []
|
|
models = {}
|
|
for name, (texlevel, mesh) in sorted(objects.items()):
|
|
if name[0] == '_':
|
|
print(name, ": texlevel =", texlevel)
|
|
models[name] = (texlevel, -1, -1)
|
|
else:
|
|
offset = len(indices)
|
|
assert offset < 65536
|
|
indices.extend(map(vertices.index, chain.from_iterable(mesh)))
|
|
count = len(indices) - offset
|
|
assert count % 3 == 0
|
|
count //= 3
|
|
assert count < 65536
|
|
print(name, ": texlevel =", texlevel, "offset =", offset, "count =", count)
|
|
models[name] = (texlevel, offset, count)
|
|
name = str(objpath.stem)
|
|
assert name not in self.vertices_db.keys()
|
|
#TODO: move to math
|
|
def pack_10(_x):
|
|
return round(_x * (512.0 if _x < 0.0 else 511.0)) & 1023
|
|
def pack_vertex(_px, _py, _pz, _nx, _ny, _nz, _s, _t):
|
|
n = (pack_10(_nx) << 20) | (pack_10(_ny) << 10) | (pack_10(_nz) << 0)
|
|
s = max(0, min(65535, round(_s * 65535.0)))
|
|
t = max(0, min(65535, round(_t * 65535.0)))
|
|
return struct.pack('fffIHH', _px, _py, _pz, n, s, t)
|
|
self.vertices_db[name] = VerticesData(name,
|
|
array('B', b''.join(starmap(pack_vertex, vertices))),
|
|
array('H', indices))
|
|
path = objpath.parent / (objpath.stem + '.hacks')
|
|
if path.exists():
|
|
hacks = self.import_hacks(path)
|
|
for name, (alias, mesh) in hacks.items():
|
|
print("Hacking", name, "from", alias, "with mesh", mesh)
|
|
assert name not in models.keys() and alias in models.keys() and mesh in models.keys()
|
|
texlevel, _, _ = models[alias]
|
|
_, offset, count = models[mesh]
|
|
models[name] = (texlevel, offset, count)
|
|
for name, (texlevel, offset, count) in sorted(models.items()):
|
|
if name[0] != '_':
|
|
print("Storing", name)
|
|
assert name not in self.models_db.keys()
|
|
self.models_db[name] = ModelData(
|
|
name, INSTANCE_FLAG_VISIBLE, texlevel, offset | count << 16)
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) < 3:
|
|
print("Usage: python3 -m tools.obj2rkar output.rkar input.obj ...")
|
|
sys.exit(2)
|
|
importer = ObjArchive()
|
|
for argv in sys.argv[2:]:
|
|
objpath = Path(argv)
|
|
importer.import_obj(objpath)
|
|
outpath = Path(sys.argv[1])
|
|
print("Exporting", outpath)
|
|
importer.save(outpath)
|
|
print("Done.")
|