rk_island/game/obj2rkar.py

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.")