UNPKG

threejs-vox-loader

Version:

Improved three.js loader for MagicaVoxel .vox files

383 lines (321 loc) 12.4 kB
import { BufferGeometry, Float32BufferAttribute, MeshStandardMaterial, PointLight, RectAreaLight, Vector3 } from "three"; import VOXSceneObject from "./VOXSceneObject.js"; function optimizeGrid(grid, w, h) { let rectangles = []; let colors = new Set(); for (let x = 0; x < w; x++) { for (let y = 0; y < h; y++) { if (grid[x][y] === null) continue; colors.add(grid[x][y]); } } if (colors.size === 0) return []; colors = [...colors]; colors.forEach(color => { let c_rectangles = []; for (let x = 0; x < w; x++) { let start_y = null; let end_y = null; for (let y = 0; y <= h; y++) { let wall = grid[x][y] === color; if (wall && y < h) { if (start_y == null) start_y = y; end_y = y; } else if (start_y !== null) { if (y == h - 1) { end_y = y - 1; } let overlaps = []; for (const r in c_rectangles) { let rect = c_rectangles[r]; if ((rect.end_x == x) && (start_y <= rect.y) && (end_y >= rect.end_y)) { overlaps.push(rect); } } overlaps.sort((a, b) => a.y - b.y); for (const o in overlaps) { let rect = overlaps[o]; if (start_y < rect.y) { c_rectangles.push({ x: x, y: start_y, end_x: x + 1, end_y: rect.y - 1, color }); start_y = rect.y; } if (start_y == rect.y) { rect.end_x++; if (end_y == rect.end_y) { start_y = null; end_y == null; } else if (end_y > rect.end_y) { start_y = rect.end_y + 1; } } } if (start_y !== null) { c_rectangles.push({ x: x, y: start_y, end_x: x + 1, end_y: end_y, color }); start_y = null; end_y = null; } } } if (start_y !== null) { c_rectangles.push({ x: x, y: start_y, end_x: x + 1, end_y: end_y, color }); start_y = null; end_y = null; } } rectangles.push(...c_rectangles); }) return rectangles.map(box => { return { a: box.x, b: box.y, end_a: box.end_x, end_b: box.end_y + 1, color: box.color } }); } export function buildSceneObject(file, obj, options) { obj.flipToThree(); const sizeX = obj.size.x; const sizeY = obj.size.y; const sizeZ = obj.size.z; const positionsByColor = {}; let lights = {}; const directions = [ { name: "x-", axis: "x", face_axis: ["y", "z"], offset: { x: -1, y: 0, z: 0 }, slices: [], getFace(base, r) { return [ base, r.a, r.b, base, r.a, r.end_b, base, r.end_a, r.b, base, r.end_a, r.end_b, base, r.end_a, r.b, base, r.a, r.end_b, ] } }, { name: "x+", axis: "x", face_axis: ["y", "z"], offset: { x: 1, y: 0, z: 0 }, slices: [], getFace(base, r) { return [ base + 1, r.a, r.b, base + 1, r.end_a, r.b, base + 1, r.a, r.end_b, base + 1, r.end_a, r.end_b, base + 1, r.a, r.end_b, base + 1, r.end_a, r.b, ] } }, { name: "y+", axis: "y", face_axis: ["x", "z"], offset: { x: 0, y: 1, z: 0 }, slices: [], getFace(base, r) { return [ r.a, base + 1, r.end_b, r.end_a, base + 1, r.end_b, r.a, base + 1, r.b, r.end_a, base + 1, r.b, r.a, base + 1, r.b, r.end_a, base + 1, r.end_b, ] } }, { name: "y-", axis: "y", face_axis: ["x", "z"], offset: { x: 0, y: -1, z: 0 }, slices: [], getFace(base, r) { return [ r.a, base, r.end_b, r.a, base, r.b, r.end_a, base, r.end_b, r.end_a, base, r.b, r.end_a, base, r.end_b, r.a, base, r.b, ] } }, { name: "z+", axis: "z", face_axis: ["x", "y"], offset: { x: 0, y: 0, z: 1 }, slices: [], getFace(base, r) { return [ r.a, r.b, base + 1, r.end_a, r.b, base + 1, r.a, r.end_b, base + 1, r.end_a, r.end_b, base + 1, r.a, r.end_b, base + 1, r.end_a, r.b, base + 1 ] } }, { name: "z-", axis: "z", face_axis: ["x", "y"], offset: { x: 0, y: 0, z: -1 }, slices: [], getFace(base, r) { return [ r.a, r.b, base, r.a, r.end_b, base, r.end_a, r.b, base, r.end_a, r.end_b, base, r.end_a, r.b, base, r.a, r.end_b, base ] } } ]; for (let x = 0; x < sizeX; x++) { for (let y = 0; y < sizeY; y++) { for (let z = 0; z < sizeZ; z++) { let current = obj.getVoxel(x, y, z); let coords = { x, y, z }; directions.forEach(dir => { let last = obj.getVoxel(x + dir.offset.x, y + dir.offset.y, z + dir.offset.z); if (last === null && current !== null) { let slice_index = coords[dir.axis]; if (!dir.slices[slice_index]) { let sliceWidth = obj.size[dir.face_axis[0]]; let sliceHeight = obj.size[dir.face_axis[1]]; let arr = new Array(sliceWidth); for (let k = 0; k < sliceWidth; k++) { arr[k] = new Array(sliceHeight); arr[k].fill(null); } dir.slices[slice_index] = arr; } let slice_coord_a = coords[dir.face_axis[0]]; let slice_coord_b = coords[dir.face_axis[1]]; dir.slices[slice_index][slice_coord_a][slice_coord_b] = current; } }) } } } directions.forEach(dir => { let sliceWidth = obj.size[dir.face_axis[0]]; let sliceHeight = obj.size[dir.face_axis[1]]; dir.slices.forEach((slice, slice_index) => { let rectangles = optimizeGrid(slice, sliceWidth, sliceHeight); rectangles.forEach(r => { if (!positionsByColor[r.color]) positionsByColor[r.color] = []; let face = dir.getFace(parseInt(slice_index), r); positionsByColor[r.color].push(...face); let voxel_coord = {}; voxel_coord[dir.axis] = parseInt(slice_index); voxel_coord[dir.face_axis[0]] = r.a; voxel_coord[dir.face_axis[1]] = r.b; const mat = file.getMaterial(r.color); if (mat?._type === "_emit") { const color = file.getThreeColor(r.color); if (options.useRectLights) { //calculate the center of the face let center = new Vector3(0, 0, 0); for (let v = 0; v < face.length; v += 3) { center.add(new Vector3(face[v + 0], face[v + 1], face[v + 2])); } center.divideScalar(face.length / 3); //offset by a tiny amount to prevent zfighting center.add(new Vector3(dir.offset.x * 0.001, dir.offset.y * 0.001, dir.offset.z * 0.001)); let rectlight = new RectAreaLight(color, options.lightIntensity, r.end_a - r.a, r.end_b - r.b); rectlight.position.set(center.x, center.y, center.z); rectlight.lookAt( rectlight.position.x + dir.offset.x, rectlight.position.y + dir.offset.y, rectlight.position.z + dir.offset.z ); lights[center.x + "," + center.y + "," + center.z] = rectlight; } else { let light = new PointLight(color, options.lightIntensity, options.lightDistance, options.lightDecay); light.position.set( voxel_coord.x + 0.5, voxel_coord.y + 0.5, voxel_coord.z + 0.5, ); lights[voxel_coord.x + "," + voxel_coord.y + "," + voxel_coord.z] = light; } } }); }) }) const geometry = new BufferGeometry(); const positions = []; const materials = []; let groupOffset = 0; for (const c in positionsByColor) { let colorpos = positionsByColor[c]; let color_index = parseInt(c); let vertexCount = colorpos.length / 3; geometry.addGroup(groupOffset, vertexCount, materials.length); positions.push(...colorpos); groupOffset += vertexCount; const mat = file.getMaterial(color_index); const color = file.getThreeColor(color_index); const opts = { ...options.defaultMaterialOptions }; if (typeof mat._type !== 'undefined') { if (options.enableEmissive && mat._type === '_emit') { opts.emissive = color; opts.emissiveIntensity = 0.5; } if (options.enableMetalness && typeof mat._metal !== "undefined") { opts.metalness = parseFloat(mat._metal); } if (options.enableRoughness && typeof mat._rough !== "undefined") { opts.roughness = parseFloat(mat._rough); } if (options.enableGlass && typeof mat._alpha !== "undefined") { opts.transparent = true; opts.opacity = mat._alpha; } } opts.color = color; let meshmat = new MeshStandardMaterial(opts); meshmat.name = color_index; materials.push(meshmat); } geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)); geometry.computeVertexNormals(); let sceneObj = new VOXSceneObject(obj); sceneObj.init(geometry, materials, Object.values(lights)); return sceneObj; }