diff --git a/src/mson/mson.type.ts b/src/mson/mson.type.ts index 9356922..ad68ca5 100644 --- a/src/mson/mson.type.ts +++ b/src/mson/mson.type.ts @@ -100,6 +100,14 @@ export interface MsonCompound extends MsonBaseComponent { } export type MsonFace = 'up' | 'down' | 'east' | 'west' | 'south' | 'north'; +export const MsonFaces: MsonFace[] = [ + 'up', + 'down', + 'east', + 'west', + 'south', + 'north', +]; export type MsonPlanarXYZWH = [ MsonVariable, diff --git a/src/test-node.ts b/src/test-node.ts index 3de583d..5ce5616 100644 --- a/src/test-node.ts +++ b/src/test-node.ts @@ -2,19 +2,19 @@ import { resolve } from 'path'; import { ModelStore, MsonEvaluate } from '.'; import { fillStoreFromFilesystem, saveGeometry } from './util/node'; import { ThreeBuilder } from './three'; -import { MeshBasicMaterial } from 'three'; +import { MeshBasicMaterial, MeshLambertMaterial } from 'three'; async function init() { const store = new ModelStore(); const evaluate = new MsonEvaluate(store); - const mat = new MeshBasicMaterial(); + const mat = new MeshLambertMaterial(); const builder = new ThreeBuilder(mat); await fillStoreFromFilesystem(store, resolve(process.cwd(), 'inputs')); // mson:steve // minelittlepony:steve_pony - const final = evaluate.evaluateModel('mson:biped'); + const final = evaluate.evaluateModel('minelittlepony:races/steve/alicorn'); // console.log(final.texture); console.dir(final._dataEvaluated, { depth: 20, diff --git a/src/three/builder.ts b/src/three/builder.ts index 220c844..0de6092 100644 --- a/src/three/builder.ts +++ b/src/three/builder.ts @@ -5,6 +5,8 @@ import { MathUtils, Mesh, Object3D, + PlaneGeometry, + Vector2, Vector3, } from 'three'; import { @@ -14,11 +16,53 @@ import { MsonComponentType, MsonCompound, MsonCompoundComponent, + MsonData, MsonEvaluatedModel, + MsonFace, + MsonFaces, + MsonPlanar, + MsonPlanarPlane, + MsonPlanarXYZWHUVXY, + MsonPlane, + MsonSlot, Vec3, } from '../mson'; +import { isArrayOfArrays } from '../util/array-of-array'; export class ThreeBuilder { + static normals: Record = { + up: new Vector3(0, -1, 0), + down: new Vector3(0, 1, 0), + east: new Vector3(1, 0, 0), + west: new Vector3(-1, 0, 0), + south: new Vector3(0, 0, 1), + north: new Vector3(0, 0, -1), + }; + + static pocConvert: Record< + MsonFace, + (pos: Vector3, size: Vector2) => Vector3 + > = { + up: function (pos: Vector3, size: Vector2): Vector3 { + return new Vector3(pos.x + size.x / 2, pos.y, pos.z + size.y / 2); + }, + down: function (pos: Vector3, size: Vector2): Vector3 { + return new Vector3(pos.x + size.x / 2, pos.y, pos.z + size.y / 2); + }, + east: function (pos: Vector3, size: Vector2): Vector3 { + return new Vector3(pos.x, pos.y + size.y / 2, pos.z + size.x / 2); + }, + west: function (pos: Vector3, size: Vector2): Vector3 { + return new Vector3(pos.x, pos.y + size.y / 2, pos.z + size.x / 2); + }, + south: function (pos: Vector3, size: Vector2): Vector3 { + return new Vector3(pos.x + size.x / 2, pos.y + size.y / 2, pos.z); + }, + north: function (pos: Vector3, size: Vector2): Vector3 { + return new Vector3(pos.x + size.x / 2, pos.y + size.y / 2, pos.z); + }, + }; + constructor(private readonly material: Material) {} /** @@ -55,15 +99,56 @@ export class ThreeBuilder { parent: Object3D, parentComponent?: MsonComponent, ) { + const wrapper = this.createWrapper( + name, + component.type || 'mson:compound', + component, + ); + parent.add(wrapper); + // Compound objects if (!component.type || component.type === 'mson:compound') { - return this.makeMsonCompound( + this.makeMsonCompound( name, component as MsonCompound, - parent, + wrapper, parentComponent, ); } + + if (component.type === 'mson:slot') { + for (const [childName, child] of Object.entries(component.data)) { + this.makeGeometry(childName, child, wrapper, component); + } + } + + if (component.type === 'mson:planar') { + this.makeMsonPlanar(name, component, wrapper, parentComponent); + } + + if (component.type === 'mson:plane') { + this.makeMsonPlane(name, component, wrapper, parentComponent); + } + + (component as MsonCompound).cubes?.forEach( + (part: MsonCompoundComponent) => { + if (!part.type || part.type === 'mson:box') { + this.makeMsonBox(name, part as MsonBox, wrapper, component); + return; + } + + if (part.type === 'mson:plane') { + this.makeMsonPlane(name, part as MsonPlane, wrapper, component); + return; + } + }, + ); + + if (component.children) { + for (const [childName, child] of Object.entries(component.children)) { + this.makeGeometry(childName, child, wrapper, component); + } + } } protected makeMsonCompound( @@ -71,17 +156,7 @@ export class ThreeBuilder { component: MsonCompound, parent: Object3D, parentComponent?: MsonComponent, - ) { - const wrapper = this.createWrapper(name, component); - parent.add(wrapper); - - component.cubes?.forEach((part: MsonCompoundComponent) => { - if (!part.type || part.type === 'mson:box') { - this.makeMsonBox(name, part as MsonBox, wrapper, component); - return; - } - }); - } + ) {} protected makeMsonBox( name: string, @@ -97,23 +172,7 @@ export class ThreeBuilder { else dilate.set(component.dilate, component.dilate, component.dilate); } - const rotate = new Vector3(); - if (parentComponent?.rotate) { - rotate.fromArray( - (parentComponent.rotate as Vec3).map((entry) => - MathUtils.degToRad(entry), - ), - ); - } - - const pos = new Vector3(); - if (parentComponent?.pivot) { - pos.fromArray(parentComponent.pivot as Vec3); - } - pos.setY(pos.y * -1); - const offset = new Vector3().fromArray(component.from as Vec3); - const halfOffset = offset.clone().divideScalar(2); const halfSize = size.clone().divideScalar(2); offset.setY(offset.y * -1); @@ -133,30 +192,157 @@ export class ThreeBuilder { adjustedTranslate.y - size.y, adjustedTranslate.z, ); - geometry.rotateX(rotate.x); - geometry.rotateY(rotate.y); - geometry.rotateZ(rotate.z); // FIXME: hack toJSON (geometry as any).type = 'BufferGeometry'; delete (geometry as any).parameters; // TODO: apply UVs - // pos.add(halfOffset).add(halfSize); const mesh = new Mesh(geometry, this.material); mesh.name = `${name}__mesh`; - mesh.position.copy(pos); mesh.updateMatrix(); parent.add(mesh); } - protected createWrapper(name: string, component: MsonBaseComponent) { + protected makeMsonPlaneFace( + face: MsonFace, + parent: Object3D, + pos: Vector3, + size: Vector2, + uv: Vector2, + mirror?: boolean | [boolean, boolean], + invertY = false, + ) { + const planeGeom = new PlaneGeometry(size.x, size.y); + let axisNormal = ThreeBuilder.normals[face]; + if (invertY) { + axisNormal = axisNormal.clone().setY(axisNormal.y * -1); + pos.setY(pos.y * -1); + if (['east', 'west', 'south', 'north'].includes(face)) + size.setY(size.y * -1); + } + const adjustedTranslate = ThreeBuilder.pocConvert[face](pos, size); + planeGeom.lookAt(axisNormal); + planeGeom.translate( + adjustedTranslate.x, + adjustedTranslate.y, + adjustedTranslate.z, + ); + + // TODO: apply UVs + + // FIXME: hack toJSON + (planeGeom as any).type = 'BufferGeometry'; + delete (planeGeom as any).parameters; + + const mesh = new Mesh(planeGeom, this.material); + // mesh.position.copy(pos); + mesh.name = face; + parent.add(mesh); + } + + protected makeMsonPlanar( + name: string, + component: MsonPlanar, + parent: Object3D, + parentComponent?: MsonComponent, + ) { + const wrapper = new Object3D(); + wrapper.name = name; + wrapper.userData.type = component.type; + parent.add(wrapper); + + for (const face of MsonFaces) { + if (!component[face]) continue; + const toMake = ( + isArrayOfArrays(component[face]) ? component[face] : [component[face]] + ) as MsonPlanarXYZWHUVXY[]; + for (const faceinfo of toMake) { + const planePos = new Vector3(); + const size = new Vector2(); + const uv = new Vector2(); + let mirror: [boolean, boolean] = [false, false]; + + planePos.fromArray(faceinfo as number[]); + // planePos.setY(planePos.y * -1); + size.fromArray(faceinfo as number[], 3); + + if (faceinfo.length > 5) { + uv.fromArray(faceinfo as number[], 5); + } + + if (faceinfo.length > 7) { + mirror = [faceinfo[7] as boolean, faceinfo[8] as boolean]; + } + this.makeMsonPlaneFace(face, wrapper, planePos, size, uv, mirror, true); + } + } + wrapper.updateMatrix(); + } + + protected makeMsonPlane( + name: string, + component: MsonPlane, + parent: Object3D, + parentComponent?: MsonComponent, + ) { + const wrapper = new Object3D(); + wrapper.name = name; + wrapper.userData.type = component.type; + parent.add(wrapper); + + const planePos = new Vector3(); + const size = new Vector2(); + const uv = new Vector2(); + + let mirror: [boolean, boolean] = [false, false]; + + if (component.position) planePos.fromArray(component.position as number[]); + if (component.size) size.fromArray(component.size as number[]); + + uv.set(component.texture?.u ?? 0, component.texture?.v ?? 0); + + if (component.mirror) mirror = component.mirror; + this.makeMsonPlaneFace( + component.face as MsonFace, + wrapper, + planePos, + size, + uv, + mirror, + true, + ); + } + + protected createWrapper( + name: string, + typeName: string, + component: MsonBaseComponent, + ) { let wrapper = new Object3D(); wrapper.name = name; - wrapper.userData.type = 'mson:compound'; + wrapper.userData.type = typeName; wrapper.visible = component.visible ?? true; + + const rotate = new Vector3(); + if (component?.rotate) { + rotate.fromArray( + (component.rotate as Vec3).map((entry, index) => + MathUtils.degToRad(index !== 1 ? entry * -1 : entry), + ), + ); + } + wrapper.rotation.setFromVector3(rotate, 'XYZ'); + + const pos = new Vector3(); + if (component?.pivot) { + pos.fromArray(component.pivot as Vec3); + } + pos.setY(pos.y * -1); + wrapper.position.copy(pos); + wrapper.updateMatrix(); return wrapper; } } diff --git a/src/util/array-of-array.ts b/src/util/array-of-array.ts new file mode 100644 index 0000000..c9a7506 --- /dev/null +++ b/src/util/array-of-array.ts @@ -0,0 +1,2 @@ +export const isArrayOfArrays = (input: any) => + Array.isArray(input) && input.every((item) => Array.isArray(item));