loaders/VOXLoader.js

/**
 * @author André Storhaug <andr3.storhaug@gmail.com>
 */

import autoBind from 'auto-bind';
import { FileLoader, Matrix4, Vector3, Loader } from 'three';
import { PointOctree } from "sparse-octree";
import formatVox from '@sh-dave/format-vox';
import { Vector4 } from 'math-ds';
import { levelOfDetail } from '../mixins/levelOfDetail';

class VOXLoader extends Loader {
  /**
   * Create a VOXLoader.
   * @classdesc Class for loading voxel data stored in VOX files.
   * @extends Loader
   * @mixes levelOfDetail
   * @param {LoadingManager} manager
   */
  constructor(manager) {
    super(manager)
    autoBind(this);
    Object.assign(this, levelOfDetail);
  }

	/**
	 * Loads and parses a VOX file from a URL.
	 *
	 * @param {String} url - URL to the VOX file.
	 * @param {Function} [onLoad] - Callback invoked with the loaded object.
	 * @param {Function} [onProgress] - Callback for download progress.
	 * @param {Function} [onError] - Callback for download errors.
	 */
  load(url, onLoad, onProgress, onError) {
    var scope = this;

    var loader = new FileLoader(this.manager);
    loader.setPath(this.path);
    loader.setResponseType('arraybuffer')
    loader.load(url, function (buffer) {

      scope.parse(buffer)
        .then(octree => onLoad(octree))
        .catch(err => console.error(err))

    }, onProgress, onError);
  }

	/**
	 * Parse VOX file data.
	 * @param {Buffer} buffer Content of VOX file.
	 * @return {Promise<PointOctree>} Promise with an octree filled with voxel data.
	 */
  parse(buffer) {
    const { VoxReader, VoxNodeTools, VoxTools } = formatVox;
    return new Promise((resolve, reject) => {
      VoxReader.read(buffer, (data, err) => {

        if (err) {
          reject(err);
        }

        let transforms = [];
        let vector = new Vector3();
        let rotation = new Matrix4();

        let positions = [];

        if (data.world == null) {
          let size = data.sizes[0];
          let modelSize = new Vector3(size.x, size.z, size.y);

          positions.push({
            model: 0,
            position: new Vector3(),
            rotation: new Vector4(),
            size: modelSize,
          });

        } else {

          VoxNodeTools.walkNodeGraph(data, {
            beginGraph: () => {
            },

            endGraph: () => {
            },

            onTransform: attributes => {
              if (VoxTools.dictHasTranslation(attributes)) {
                const t = VoxTools.getTranslationFromDict(attributes);
                transforms.push(new Vector3(t.x, t.z, t.y))
                vector.add(new Vector3(t.x, t.z, t.y))
              } else {
                transforms.push(new Vector3())
              }

              if (VoxTools.dictHasRotation(attributes)) {
                const r = VoxTools.getRotationFromDict(attributes);
                let m = new Matrix4();
                m.set(
                  r._00, r._01, r._02, 0,
                  r._10, r._11, r._12, 0,
                  r._20, r._21, r._22, 0,
                  0, 0, 0, 1
                );

                transforms.push(m)
                rotation.multiply(m);
              } else {
                transforms.push(new Matrix4())
              }
            },

            beginGroup: () => {
            },

            endGroup: () => {
              let m = transforms.pop();
              let vec = transforms.pop();
              vector.sub(vec);
              rotation.multiply(m.getInverse(m));
            },

            onShape: (attributes, models) => {
              let modelId = models[0].modelId;
              let position = new Vector3().add(vector);
              let rotVec = new Matrix4().multiply(rotation);

              let size = data.sizes[modelId];
              let modelSize = new Vector3(size.x, size.z, size.y);

              positions.push({
                model: modelId,
                position: position,
                rotation: rotVec,
                size: modelSize,
              });

              let m = transforms.pop();
              let vec = transforms.pop();
              vector.sub(vec);
              rotation.multiply(m.getInverse(m));
            }
          });
        }

        let xMin = Infinity;
        let yMin = Infinity;
        let zMin = Infinity;

        let xMax = -Infinity;
        let yMax = -Infinity;
        let zMax = -Infinity;

        for (let i = 0; i < positions.length; i++) {
          const element = positions[i];
          const position = element.position;
          const size = element.size;

          if (position.x < xMin) xMin = position.x - size.x / 2;
          if (position.y < yMin) yMin = position.y - size.y / 2;
          if (position.z < zMin) zMin = position.z - size.z / 2;

          if (position.x > xMax) xMax = position.x + size.x / 2;
          if (position.y > yMax) yMax = position.y + size.y / 2;
          if (position.z > zMax) zMax = position.z + size.z / 2;
        }

        const min = new Vector3(xMin, yMin, zMin);
        const max = new Vector3(xMax, yMax, zMax);

        let octree = new PointOctree(min, max, 0, this.LOD.maxPoints, this.LOD.maxDepth);

        for (let i = 0; i < positions.length; i++) {
          let model = positions[i].model;
          let pos = positions[i].position;
          let size = positions[i].size;
          let worldCorrection = new Vector3().copy(size).divideScalar(2);

          for (let j = 0; j < data.models[model].length; j++) {
            const element = data.models[model][j];
            const color = data.palette[element.colorIndex];

            var voxelData = { color: { r: color.r, g: color.g, b: color.b } };
            let position = new Vector3(element.x, element.z, element.y);

            position.sub(worldCorrection);
            // TODO fix rotation matrix basis
            //position.applyMatrix4(rot
            position.add(pos);

            octree.insert(position, voxelData);
          }
        }

        resolve(octree);
      });
    });
  }

}

export { VOXLoader };