improve export_nif script and process textures, make query hash case-insentisive

master
eater 5 years ago
parent c47d712624
commit 76f50d2f5a
Signed by: eater
GPG Key ID: AD2560A0F84F0759

4
.gitignore vendored

@ -1,5 +1,5 @@
dist dist
node_modules node_modules
public/blob/textures/* public/blob/texture/*
!public/blob/textures/.gitkeep !public/blob/texture/.gitkeep
public/blob/meshes.json public/blob/meshes.json

@ -7,17 +7,15 @@ A project that takes morrowind's player heads and shows it on a webpage - [see i
## Requirements ## Requirements
- pyFFI - pyFFI
- PIL / Pillow
- yarn / npm - yarn / npm
# Setup # Setup
```bash ```bash
cd [project] cd [project]
# Copy textures
cp [morrowind]/Data\ Files/Textures/TX_B_N_* ./public/blob/textures
# Generate JSON meshes # Generate JSON meshes
./script/export_nif.py [morrowind]/Data\ Files/Meshes/b > ./public/blob/meshes.json ./script/export_nif.py [morrowind]/Data\ Files/Meshes/b [morrowind]/Data\ Files/Textures public/blob
# Pack JS # Pack JS
yarn yarn
yarn webpack yarn webpack

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Morrowind's Heads</title>
<style> <style>
body { body {
background-color: #ffffff; background-color: #ffffff;
@ -85,6 +86,6 @@
</div> </div>
</div> </div>
<script src="js/dist/bundle.js"></script> <script src="js/dist/bundle.js?v=5"></script>
</body> </body>
</html> </html>

@ -1,12 +1,78 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys import sys
from os import DirEntry
from typing import List, Tuple, Optional, Dict
from pyffi.formats.nif import NifFormat from pyffi.formats.nif import NifFormat
from PIL import Image
import json import json
def export(path): class Texture:
file: str
wrap_s: bool
wrap_t: bool
uv_set: int
def __init__(self, file: str, wrap_s: bool = True, wrap_t: bool = True, uv_set: int = 0):
self.file = file
self.wrap_s = wrap_s
self.wrap_t = wrap_t
self.uv_set = uv_set
def __dict__(self):
return {
'file': self.file,
'wrapS': self.wrap_s,
'wrapT': self.wrap_t,
'uvSet': self.uv_set,
}
class Shape:
name: str
vertices: List[Tuple[float, float, float]]
normals: List[Tuple[float, float, float]]
faces: List[Tuple[float, float, float]]
uv_sets: List[List[float]]
translation: Tuple[float, float, float]
rotation: List[Tuple[float, float, float]]
texture: Optional[Texture]
def __init__(self, name: str = ""):
self.name = name
self.texture = None
def __dict__(self):
return {
'name': self.name,
'vertices': self.vertices,
'normals': self.normals,
'faces': self.faces,
'uvSets': self.uv_sets,
'translation': self.translation,
'rotation': self.rotation,
'texture': self.texture.__dict__(),
}
class ShapeCollection:
name: str
shapes: List[Shape]
def __init__(self, name: str = ""):
self.name = name
self.shapes = []
def __dict__(self):
return {
'name': self.name,
'shapes': [s.__dict__() for s in self.shapes],
}
def export(path) -> List[ShapeCollection]:
data = NifFormat.Data() data = NifFormat.Data()
with open(path, 'rb') as f: with open(path, 'rb') as f:
data.inspect(f) data.inspect(f)
@ -27,67 +93,135 @@ def export(path):
# Some NiTriShape's are a root object # Some NiTriShape's are a root object
if isinstance(root, NifFormat.NiTriShape): if isinstance(root, NifFormat.NiTriShape):
objects.append({'name': root.name.decode('utf-8'), 'shapes': [export_shape(root)]}) coll = ShapeCollection(name)
coll.shapes = [export_shape(root)]
objects.append(coll)
return objects return objects
def export_object(root: NifFormat.NiNode): def export_object(root: NifFormat.NiNode) -> List[ShapeCollection]:
obj = {'name': root.name.decode("utf-8"), 'shapes': []} obj = ShapeCollection(root.name.decode('utf-8'))
for shape in root.get_children(): for shape in root.tree():
if not isinstance(shape, NifFormat.NiTriShape): if not isinstance(shape, NifFormat.NiTriShape):
continue continue
obj['shapes'].append(export_shape(shape)) obj.shapes.append(export_shape(shape))
return obj return obj
def export_shape(shape: NifFormat.NiTriShape): def export_shape(shape: NifFormat.NiTriShape) -> Shape:
vertices = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.vertices)] vertices = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.vertices)]
faces = shape.data.get_triangles() faces = shape.data.get_triangles()
uv_sets = [[[coord.u, coord.v] for coord in uv_set] for uv_set in shape.data.uv_sets] uv_sets = [[(coord.u, coord.v) for coord in uv_set] for uv_set in shape.data.uv_sets]
normals = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.normals)] normals = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.normals)]
texture = None
res_shape = Shape()
for property in shape.get_properties(): for property in shape.get_properties():
if isinstance(property, NifFormat.NiTexturingProperty): if isinstance(property, NifFormat.NiTexturingProperty):
texture = export_texture(property.base_texture) res_shape.texture = export_texture(property.base_texture)
res_shape.name = shape.name.decode('utf-8')
res_shape.vertices = vertices
res_shape.faces = faces
res_shape.uv_sets = uv_sets
res_shape.normals = normals
res_shape.translation = (shape.translation.x, shape.translation.y, shape.translation.z)
res_shape.rotation = shape.rotation.as_list()
return { return res_shape
'name': shape.name.decode("utf-8"),
'vertices': vertices,
'faces': faces,
'uvSets': uv_sets,
'normals': normals,
'texture': texture,
'translation': (shape.translation.x, shape.translation.y, shape.translation.z),
'rotation': shape.rotation.as_list(),
}
def export_texture(texture): def export_texture(texture) -> Texture:
wrap_s = texture.clamp_mode in [2, 3] wrap_s = texture.clamp_mode in [2, 3]
wrap_t = texture.clamp_mode in [1, 3] wrap_t = texture.clamp_mode in [1, 3]
source = texture.source source = texture.source
return { return Texture(source.file_name.decode('utf-8'), wrap_s, wrap_t, texture.uv_set)
'wrapS': wrap_s,
'wrapT': wrap_t,
'uvSet': texture.uv_set,
'file': source.file_name.decode("utf-8")
}
if __name__ == '__main__': # Dumb file map allowing case-insensitive matching
# And Morrowind's fucky file resolving
class TextureMap:
path: str
files: Dict[str, DirEntry]
dds_used: int
original_dds: int
original_used: int
def __init__(self, path):
self.path = path
self.files = {}
self.dds_used = 0
self.original_dds = 0
self.original_used = 0
def build(self):
for item in os.scandir(self.path):
self.files[item.name.lower()] = item
def get(self, name):
lower_name = name.lower()
dds_instead = lower_name.rsplit('.', maxsplit=1)[0] + '.dds'
if dds_instead in self.files:
if dds_instead == lower_name:
self.original_dds += 1
else:
self.dds_used += 1
return self.files[dds_instead].path
self.original_used += 1
return self.files[lower_name].path
def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <dir>") print(f"Usage: {sys.argv[0]} <nif dir> <texture dir> <output dir>")
exit(1) exit(1)
objects = [] nif_dir = sys.argv[1]
for item in os.scandir(sys.argv[1]): texture_dir = sys.argv[2]
if item.is_file(): texture_dir_map = TextureMap(texture_dir)
objects.extend(export(item.path)) texture_dir_map.build()
output_dir = sys.argv[3]
output_texture_dir = os.path.join(output_dir, 'texture')
os.makedirs(output_texture_dir, exist_ok=True)
print(json.dumps(objects)) output_mesh_file = os.path.join(output_dir, 'meshes.json')
shape_colls = []
for item in os.scandir(nif_dir):
if not item.is_file():
continue
file_objects = export(item.path)
for file_object in file_objects:
file_object.name = file_object.name.lower()
for shape in file_object.shapes:
if shape.texture is not None:
new_texture_name = shape.texture.file.rsplit('.', maxsplit=1)[0] + '.png'
new_texture_name = new_texture_name.lower()
new_file_name = os.path.join(output_texture_dir, new_texture_name)
texture_image = Image.open(texture_dir_map.get(shape.texture.file))
texture_image.save(new_file_name, 'png')
shape.texture.file = new_texture_name
shape_colls.append(file_object.__dict__())
with open(output_mesh_file, 'w') as j:
json.dump(shape_colls, j)
print(f"Textures: {texture_dir_map.original_dds} Orig. DDS, {texture_dir_map.dds_used} New DDS, {texture_dir_map.original_used} Orig. Non-DDS")
if __name__ == '__main__':
main()

@ -19,32 +19,42 @@ const nextHair = document.querySelector('#next_hair');
const prevHead = document.querySelector('#prev_head'); const prevHead = document.querySelector('#prev_head');
const nextHead = document.querySelector('#next_head'); const nextHead = document.querySelector('#next_head');
const RACES = {
'darkelf': 'Dark Elf',
'khajiit': 'Khajiit',
'woodelf': 'Wood Elf',
'highelf': 'High Elf',
'redguard': 'Redguard',
'breton': 'Breton',
'argonian': 'Argonian',
'orc': 'Orc',
'imperial': 'Imperial',
'nord': 'Nord',
};
window.addEventListener("load", async () => { window.addEventListener("load", async () => {
const allMeshes = await (await fetch("blob/meshes.json")).json(); const allMeshes = await (await fetch("blob/meshes.json?v=2")).json();
part1.innerHTML = ""; part1.innerHTML = "";
part2.innerHTML = ""; part2.innerHTML = "";
const indexed = window.indexed = {}; const indexed = window.indexed = {};
const byName = window.byName = {}; const byName = window.byName = {};
const tree = {}; const tree = window.tree = {};
const shapeCache = {};
headRenderer.init(); headRenderer.init();
headRenderer.expose(); headRenderer.expose();
for (let mesh of allMeshes) { for (let mesh of allMeshes) {
let [_, __, race, gender, part, nr = undefined] = mesh.name.split('_'); let [_, __, race = "object", gender = "X", part = "head", nr = undefined] = mesh.name.split('_');
race = race.replace(new RegExp("\\s+"), ' '); race = race.replace(new RegExp("\\s+"), '');
race = RACES[race] || 'Object';
if (race === 'DarkElf') {
race = 'Dark Elf'
}
if (nr === undefined && !['head', 'hair'].includes(part.toLowerCase())) { if (nr === undefined && !['head', 'hair'].includes(part.toLowerCase())) {
[_, part, nr] = part.match(/(head|hair)(\d+)?/i) || []; [_, part = "head", nr] = part.match(/(head|hair)(\d+)?/i) || [];
} }
gender = gender.toUpperCase();
let option = document.createElement("option"); let option = document.createElement("option");
option.textContent = mesh.name; option.textContent = mesh.name;
option.value = mesh.id = uuidv4(); option.value = mesh.id = uuidv4();
@ -119,7 +129,7 @@ window.addEventListener("load", async () => {
function collectHashQuery() { function collectHashQuery() {
return location.hash.substr(1).split('&').reduce((c, x) => { return location.hash.substr(1).split('&').reduce((c, x) => {
let [name, value = ""] = x.split('=', 2).map(y => decodeURIComponent(y)); let [name, value = ""] = x.split('=', 2).map(y => decodeURIComponent(y));
c[name] = value; c[name] = value.toLowerCase();
return c; return c;
}, {}); }, {});
} }
@ -153,7 +163,7 @@ window.addEventListener("load", async () => {
}); });
function getShape(name) { function getShape(name) {
return new ShapeCollection(indexed[name], "blob/textures/"); return new ShapeCollection(indexed[name], "blob/texture/");
} }
function updateHash() { function updateHash() {
@ -161,6 +171,8 @@ window.addEventListener("load", async () => {
} }
function setHair(id) { function setHair(id) {
if (!id) return;
part1.value = id; part1.value = id;
if (shape1 !== null) { if (shape1 !== null) {
headRenderer.remove(shape1); headRenderer.remove(shape1);
@ -184,6 +196,8 @@ window.addEventListener("load", async () => {
}); });
function setHead(id) { function setHead(id) {
if (!id) return;
part2.value = id; part2.value = id;
if (shape2 !== null) { if (shape2 !== null) {
headRenderer.remove(shape2); headRenderer.remove(shape2);

Loading…
Cancel
Save