Initial commit
commit
cdb6c25aae
@ -0,0 +1,5 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
public/blob/textures/*
|
||||||
|
!public/blob/textures/.gitkeep
|
||||||
|
public/blob/meshes.json
|
@ -0,0 +1,30 @@
|
|||||||
|
# Morrowind Three.JS
|
||||||
|
|
||||||
|
A project that takes morrowind's player heads and shows it on a webpage
|
||||||
|
|
||||||
|
![](examples/Z9x9jF6.png)
|
||||||
|
![](examples/ZmJe6PQ.png)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- pyFFI
|
||||||
|
- yarn / npm
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd [project]
|
||||||
|
|
||||||
|
# Copy textures
|
||||||
|
cp [morrowind]/Data\ Files/Textures/TX_B_N_* ./public/blob/textures
|
||||||
|
# Generate JSON meshes
|
||||||
|
./script/export_nif.py [morrowind]/Data\ Files/Meshes/b > ./public/blob/meshes.json
|
||||||
|
# Pack JS
|
||||||
|
yarn
|
||||||
|
yarn webpack
|
||||||
|
# Run dev http server via PHP e.g.
|
||||||
|
cd public;
|
||||||
|
php -S 0:8888;
|
||||||
|
```
|
||||||
|
|
||||||
|
meshes.json is not included because it's easily around 15MB
|
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
After Width: | Height: | Size: 268 KiB |
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "morrowinds-threejs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"three": "^0.113.2",
|
||||||
|
"webpack": "^4.41.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"css-loader": "^3.4.2",
|
||||||
|
"file-loader": "^5.1.0",
|
||||||
|
"source-map-loader": "^0.2.4",
|
||||||
|
"style-loader": "^1.1.3",
|
||||||
|
"webpack-cli": "^3.3.11"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
margin: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard {
|
||||||
|
position: absolute;
|
||||||
|
padding: 25px;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="overlay">
|
||||||
|
<div class="part-1">
|
||||||
|
<label for="part_1">Hair:</label>
|
||||||
|
<select id="part_1">
|
||||||
|
<option>Loading...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="part-2">
|
||||||
|
<label for="part_2">Head:</label>
|
||||||
|
<select id="part_2">
|
||||||
|
<option>Loading...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wizard">
|
||||||
|
<div class="race">
|
||||||
|
<label for="race">Race: </label>
|
||||||
|
<select name="race" id="race">
|
||||||
|
<option>Loading...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="gender">
|
||||||
|
<label for="gender">Gender: </label>
|
||||||
|
<select name="gender" id="gender">
|
||||||
|
<option>Loading...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="hair">
|
||||||
|
<button id="prev_hair"><</button>
|
||||||
|
Hair
|
||||||
|
<button id="next_hair">></button>
|
||||||
|
</div>
|
||||||
|
<div class="head">
|
||||||
|
<button id="prev_head"><</button>
|
||||||
|
Head
|
||||||
|
<button id="next_head">></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/dist/bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pyffi.formats.nif import NifFormat
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def export(path):
|
||||||
|
data = NifFormat.Data()
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
data.inspect(f)
|
||||||
|
data.read(f)
|
||||||
|
|
||||||
|
objects = []
|
||||||
|
for root in data.roots:
|
||||||
|
name = root.name.decode('utf-8').lower()
|
||||||
|
# For this project, only hair and head matters
|
||||||
|
if 'hair' in name or 'head' in name:
|
||||||
|
sys.stderr.write("> " + name + "\n")
|
||||||
|
else:
|
||||||
|
sys.stderr.write(" " + name + "\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(root, NifFormat.NiNode):
|
||||||
|
objects.append(export_object(root))
|
||||||
|
|
||||||
|
# Some NiTriShape's are a root object
|
||||||
|
if isinstance(root, NifFormat.NiTriShape):
|
||||||
|
objects.append({'name': root.name.decode('utf-8'), 'shapes': [export_shape(root)]})
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
|
||||||
|
def export_object(root: NifFormat.NiNode):
|
||||||
|
obj = {'name': root.name.decode("utf-8"), 'shapes': []}
|
||||||
|
|
||||||
|
for shape in root.get_children():
|
||||||
|
if not isinstance(shape, NifFormat.NiTriShape):
|
||||||
|
continue
|
||||||
|
|
||||||
|
obj['shapes'].append(export_shape(shape))
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def export_shape(shape: NifFormat.NiTriShape):
|
||||||
|
vertices = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.vertices)]
|
||||||
|
faces = shape.data.get_triangles()
|
||||||
|
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)]
|
||||||
|
texture = None
|
||||||
|
|
||||||
|
for property in shape.get_properties():
|
||||||
|
if isinstance(property, NifFormat.NiTexturingProperty):
|
||||||
|
texture = export_texture(property.base_texture)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'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):
|
||||||
|
wrap_s = texture.clamp_mode in [2, 3]
|
||||||
|
wrap_t = texture.clamp_mode in [1, 3]
|
||||||
|
source = texture.source
|
||||||
|
|
||||||
|
return {
|
||||||
|
'wrapS': wrap_s,
|
||||||
|
'wrapT': wrap_t,
|
||||||
|
'uvSet': texture.uv_set,
|
||||||
|
'file': source.file_name.decode("utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} <dir>")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
objects = []
|
||||||
|
for item in os.scandir(sys.argv[1]):
|
||||||
|
if item.is_file():
|
||||||
|
objects.extend(export(item.path))
|
||||||
|
|
||||||
|
print(json.dumps(objects))
|
@ -0,0 +1,66 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import {Color, Group} from 'three'
|
||||||
|
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";
|
||||||
|
|
||||||
|
export class HeadRenderer {
|
||||||
|
constructor() {
|
||||||
|
this.shapes = {};
|
||||||
|
this.scene = null;
|
||||||
|
this.renderer = null;
|
||||||
|
this.camera = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const W = window.innerWidth, H = window.innerHeight;
|
||||||
|
|
||||||
|
this.renderer = new THREE.WebGLRenderer();
|
||||||
|
this.renderer.setSize(W, H);
|
||||||
|
document.body.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
this.camera = new THREE.PerspectiveCamera(70, W / H, 1, 2000);
|
||||||
|
this.camera.position.z = 600;
|
||||||
|
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new Color(0xffffff);
|
||||||
|
|
||||||
|
let light = new THREE.PointLight(0xffffff, 1);
|
||||||
|
light.position.set(1, 1, 2);
|
||||||
|
|
||||||
|
// Make sure light follow the camera
|
||||||
|
this.camera.add(light);
|
||||||
|
|
||||||
|
// Add camera to scene so light gets rendered
|
||||||
|
this.scene.add(this.camera);
|
||||||
|
|
||||||
|
let ambientLight = new THREE.AmbientLight(0x404040, 1);
|
||||||
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
|
new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
|
||||||
|
if (typeof __THREE_DEVTOOLS__ !== 'undefined') {
|
||||||
|
__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent('observe', {detail: this.scene}));
|
||||||
|
__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent('observe', {detail: this.renderer}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
window.requestAnimationFrame(() => this.draw());
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(shapeCollection) {
|
||||||
|
this.shapes[shapeCollection.id] = shapeCollection;
|
||||||
|
this.scene.add(shapeCollection.getGroup());
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(shapeCollection) {
|
||||||
|
delete this.shapes[shapeCollection.id];
|
||||||
|
this.scene.remove(shapeCollection.getGroup());
|
||||||
|
}
|
||||||
|
|
||||||
|
expose() {
|
||||||
|
window.scene = this.scene;
|
||||||
|
window.camera = this.camera;
|
||||||
|
window.renderer = this.renderer;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
Face3,
|
||||||
|
Geometry, Matrix4,
|
||||||
|
Mesh, MeshPhongMaterial,
|
||||||
|
RepeatWrapping,
|
||||||
|
TextureLoader,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
} from 'three'
|
||||||
|
import {uuidv4} from "./util";
|
||||||
|
import {TGALoader} from "three/examples/jsm/loaders/TGALoader";
|
||||||
|
import {DDSLoader} from "three/examples/jsm/loaders/DDSLoader";
|
||||||
|
|
||||||
|
export class Shape {
|
||||||
|
constructor({
|
||||||
|
vertices = [],
|
||||||
|
faces = [],
|
||||||
|
texture = null,
|
||||||
|
uvSets = [],
|
||||||
|
normals = [],
|
||||||
|
translation = [0, 0, 0],
|
||||||
|
rotation
|
||||||
|
} = {}, filePrefix = "") {
|
||||||
|
this.id = uuidv4();
|
||||||
|
|
||||||
|
this.vertices = vertices;
|
||||||
|
this.translation = translation;
|
||||||
|
this.rotation = rotation;
|
||||||
|
this.faces = faces;
|
||||||
|
this.textureInfo = texture;
|
||||||
|
this.normals = normals;
|
||||||
|
this.uvSets = uvSets;
|
||||||
|
|
||||||
|
this.filePrefix = filePrefix;
|
||||||
|
|
||||||
|
this.geometry = null;
|
||||||
|
this.mesh = null;
|
||||||
|
this.texture = null;
|
||||||
|
this.material = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaterial() {
|
||||||
|
if (this.material) {
|
||||||
|
return this.material;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
color: 0xffffff,
|
||||||
|
transparent: true,
|
||||||
|
alphaTest: 0.5,
|
||||||
|
};
|
||||||
|
if (this.textureInfo) {
|
||||||
|
let Loader;
|
||||||
|
|
||||||
|
if (this.textureInfo.file.toLowerCase().endsWith('.tga')) {
|
||||||
|
Loader = TGALoader
|
||||||
|
} else if (this.textureInfo.file.toLowerCase().endsWith('.dds')) {
|
||||||
|
Loader = DDSLoader
|
||||||
|
} else {
|
||||||
|
Loader = TextureLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
this.texture = new Loader().load(this.filePrefix + this.textureInfo.file, () => {
|
||||||
|
}, null, (err) => {
|
||||||
|
console.log(err)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.textureInfo.wrapS) {
|
||||||
|
this.texture.wrapS = RepeatWrapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.textureInfo.wrapT) {
|
||||||
|
this.texture.wrapT = RepeatWrapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.texture.flipY = false;
|
||||||
|
params.map = this.texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.material = new MeshPhongMaterial(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGeometry() {
|
||||||
|
if (this.geometry) {
|
||||||
|
return this.geometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = this.geometry = new Geometry();
|
||||||
|
|
||||||
|
for (let [x, y, z] of this.vertices) {
|
||||||
|
geometry.vertices.push(new Vector3(x, y, z))
|
||||||
|
}
|
||||||
|
|
||||||
|
const uvSets = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.uvSets.length; i++) {
|
||||||
|
uvSets.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.faces.length; i++) {
|
||||||
|
let [a, b, c] = this.faces[i];
|
||||||
|
|
||||||
|
// Add faces with normals
|
||||||
|
// Normals are stored per vector
|
||||||
|
geometry.faces.push(new Face3(a, b, c, [
|
||||||
|
new Vector3(this.normals[a][0], this.normals[a][1], this.normals[a][2]),
|
||||||
|
new Vector3(this.normals[b][0], this.normals[b][1], this.normals[b][2]),
|
||||||
|
new Vector3(this.normals[c][0], this.normals[c][1], this.normals[c][2])
|
||||||
|
]));
|
||||||
|
|
||||||
|
for (let j = 0; j < this.uvSets.length; j++) {
|
||||||
|
uvSets[j].push([
|
||||||
|
new Vector2(this.uvSets[j][a][0], this.uvSets[j][a][1]),
|
||||||
|
new Vector2(this.uvSets[j][b][0], this.uvSets[j][b][1]),
|
||||||
|
new Vector2(this.uvSets[j][c][0], this.uvSets[j][c][1]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.faceVertexUvs = uvSets;
|
||||||
|
|
||||||
|
// First rotate
|
||||||
|
let m = new Matrix4();
|
||||||
|
m.makeBasis(new Vector3(...this.rotation[0]), new Vector3(...this.rotation[1]), new Vector3(...this.rotation[2]));
|
||||||
|
geometry.applyMatrix4(m);
|
||||||
|
|
||||||
|
// Then translate
|
||||||
|
geometry.translate(this.translation[0], this.translation[1], this.translation[2]);
|
||||||
|
|
||||||
|
// Scale up a bit
|
||||||
|
geometry.scale(20, 20, 20);
|
||||||
|
|
||||||
|
// Rotate the face so it faces us \o/
|
||||||
|
geometry.rotateX(290 * (Math.PI / 180));
|
||||||
|
|
||||||
|
geometry.computeFaceNormals();
|
||||||
|
geometry.computeVertexNormals();
|
||||||
|
|
||||||
|
return this.geometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMesh() {
|
||||||
|
if (this.mesh) {
|
||||||
|
return this.mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mesh = new Mesh(this.getGeometry(), this.getMaterial());
|
||||||
|
return this.mesh;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import {Shape} from "./Shape";
|
||||||
|
import {Group} from "three";
|
||||||
|
import {uuidv4} from "./util";
|
||||||
|
|
||||||
|
export class ShapeCollection {
|
||||||
|
constructor({shapes = [], name = ""}, filePrefix = "") {
|
||||||
|
this.id = uuidv4();
|
||||||
|
this.shapes = shapes.map((s) => new Shape(s, filePrefix));
|
||||||
|
this.group = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroup() {
|
||||||
|
if (this.group) {
|
||||||
|
return this.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create group for all shapes
|
||||||
|
this.group = new Group();
|
||||||
|
|
||||||
|
for (let shape of this.shapes) {
|
||||||
|
this.group.add(shape.getMesh())
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.group
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
import {HeadRenderer} from "./HeadRenderer";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import {ShapeCollection} from "./ShapeCollection";
|
||||||
|
import {uuidv4} from "./util";
|
||||||
|
|
||||||
|
const headRenderer = window.headRenderer = new HeadRenderer();
|
||||||
|
|
||||||
|
const part1 = document.querySelector("#part_1");
|
||||||
|
let shape1 = null;
|
||||||
|
const part2 = document.querySelector("#part_2");
|
||||||
|
let shape2 = null;
|
||||||
|
|
||||||
|
const race = document.querySelector("#race");
|
||||||
|
const gender = document.querySelector("#gender");
|
||||||
|
|
||||||
|
const prevHair = document.querySelector('#prev_hair');
|
||||||
|
const nextHair = document.querySelector('#next_hair');
|
||||||
|
|
||||||
|
const prevHead = document.querySelector('#prev_head');
|
||||||
|
const nextHead = document.querySelector('#next_head');
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener("load", async () => {
|
||||||
|
const allMeshes = await (await fetch("blob/meshes.json")).json();
|
||||||
|
|
||||||
|
part1.innerHTML = "";
|
||||||
|
part2.innerHTML = "";
|
||||||
|
const indexed = {};
|
||||||
|
const tree = {};
|
||||||
|
const shapeCache = {};
|
||||||
|
|
||||||
|
headRenderer.init();
|
||||||
|
headRenderer.expose();
|
||||||
|
|
||||||
|
for (let mesh of allMeshes) {
|
||||||
|
let [_, x, race, gender, part, nr = undefined] = mesh.name.split('_');
|
||||||
|
race = race.replace(new RegExp("\\s+"), ' ');
|
||||||
|
if (race.toLowerCase() === "khajiit") {
|
||||||
|
console.log(part, mesh.name, nr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x !== 'N') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nr === undefined) {
|
||||||
|
[_, part, nr] = part.match(/(head|hair)(\d+)?/i) || [];
|
||||||
|
console.log(part);
|
||||||
|
|
||||||
|
if (!nr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let option = document.createElement("option");
|
||||||
|
option.textContent = mesh.name;
|
||||||
|
option.value = mesh.id = uuidv4();
|
||||||
|
indexed[mesh.id] = mesh;
|
||||||
|
|
||||||
|
|
||||||
|
if (mesh.name.toLowerCase().indexOf("hair") !== -1) {
|
||||||
|
part1.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mesh.name.toLowerCase().indexOf("head") !== -1) {
|
||||||
|
part2.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree[race]) {
|
||||||
|
tree[race] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree[race][gender]) {
|
||||||
|
tree[race][gender] = {'head': [], 'hair': []}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree[race][gender][part.toLowerCase()].push(mesh.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
race.innerHTML = "";
|
||||||
|
for (let raceX in tree) {
|
||||||
|
let option = document.createElement("option");
|
||||||
|
option.textContent = raceX;
|
||||||
|
option.value = raceX;
|
||||||
|
race.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
race.addEventListener('change', () => {
|
||||||
|
setRace(race.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setRace(name) {
|
||||||
|
race.value = name;
|
||||||
|
gender.innerHTML = "";
|
||||||
|
for (let genderX in tree[name]) {
|
||||||
|
let option = document.createElement("option");
|
||||||
|
option.textContent = genderX;
|
||||||
|
option.value = genderX;
|
||||||
|
gender.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGender(gender.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
gender.addEventListener("change", () => {
|
||||||
|
setGender(gender.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setGender(name) {
|
||||||
|
gender.value = name;
|
||||||
|
setHair(tree[race.value][name]['hair'][0]);
|
||||||
|
setHead(tree[race.value][name]['head'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRace("Dark Elf");
|
||||||
|
setGender("M");
|
||||||
|
|
||||||
|
function getShape(name) {
|
||||||
|
return new ShapeCollection(indexed[name], "blob/textures/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHair(id) {
|
||||||
|
part1.value = id;
|
||||||
|
if (shape1 !== null) {
|
||||||
|
headRenderer.remove(shape1);
|
||||||
|
}
|
||||||
|
headRenderer.add(shape1 = getShape(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
part1.addEventListener("change", () => {
|
||||||
|
setHair(part1.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
prevHair.addEventListener('click', () => {
|
||||||
|
let hairList = tree[race.value][gender.value]['hair'];
|
||||||
|
setHair(hairList[(hairList.length + (hairList.indexOf(part1.value) - 1)) % hairList.length]);
|
||||||
|
});
|
||||||
|
|
||||||
|
nextHair.addEventListener('click', () => {
|
||||||
|
let hairList = tree[race.value][gender.value]['hair'];
|
||||||
|
setHair(hairList[(hairList.indexOf(part1.value) + 1) % hairList.length]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setHead(id) {
|
||||||
|
part2.value = id;
|
||||||
|
if (shape2 !== null) {
|
||||||
|
headRenderer.remove(shape2);
|
||||||
|
}
|
||||||
|
headRenderer.add(shape2 = getShape(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
part2.addEventListener("change", () => {
|
||||||
|
setHead(part2.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
prevHead.addEventListener('click', () => {
|
||||||
|
let headList = tree[race.value][gender.value]['head'];
|
||||||
|
setHead(headList[(headList.length + (headList.indexOf(part2.value) - 1)) % headList.length]);
|
||||||
|
});
|
||||||
|
|
||||||
|
nextHead.addEventListener('click', () => {
|
||||||
|
let headList = tree[race.value][gender.value]['head'];
|
||||||
|
setHead(headList[(headList.indexOf(part2.value) + 1) % headList.length]);
|
||||||
|
});
|
||||||
|
|
||||||
|
headRenderer.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.THREE = THREE;
|
@ -0,0 +1,6 @@
|
|||||||
|
// I found this on the internet
|
||||||
|
export function uuidv4() {
|
||||||
|
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||||
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/index.js',
|
||||||
|
mode: 'development',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'public/js/dist'),
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue