add basic idea for .vox exporter

This commit is contained in:
HF 2020-05-04 22:32:53 +02:00
parent 0e48307613
commit 3b376c30d4
2 changed files with 168 additions and 2 deletions

167
src/core/voxExport.js Normal file
View File

@ -0,0 +1,167 @@
/*
* .vox file exporter for 3D canvas
* .vox is the Magica Voxel file format that is also compatible with
* other vodel editors like Goxel.
* A object in a .vox file can have a max dimension of 128x128 and 256 height
* As of the latest release with 0.99.5 4/5/2020 this limit is now 256x256x256,
* however, lets keep with the old restreints for support of other software.
* In .vox, 0,0 is the corner of the model and coordinates are unsigned int,
* the z dimension is the height, contrarian to pixelplanet with y.
*
* Reference:
* https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt
*
* @flow
*/
/*
* THIS IS JUST THE START, I WONT CONTINUE IT ANYTIME SOON SO ITS ALREADY
* COMMITED AS A REFERENCE AND IDEA
* The idea is to export a 128x128 area around the focal point of where the user
* is currently looking at as .vox. The file should just have one model.
*/
import { THREE_TILE_SIZE, THREE_CANVAS_HEIGHT } from './constants';
const VOX_OBJECT_SIZE = 128;
const VOX_OBJECT_CSIZE = VOX_OBJECT_SIZE / THREE_TILE_SIZE;
function calcChunkSize(buffer) {
let cnt = 0;
let u = buffer.length;
while (u >= 0) {
u -= 1;
if (buffer[u] !== 0) {
cnt += 1;
}
}
return cnt;
}
async function exportVox(
chunks,
canvas,
centerPoint,
) {
const { size: canvasSize, colors } = canvas;
// round to chunk with its -x and -y corner closest to centerPoint
const [xc,, yc] = centerPoint.map((z) => {
const zabs = z + (canvasSize / 2);
return Math.round(zabs / THREE_TILE_SIZE);
});
const [xcMin, ycMin] = [xc, yc].map((zc) => {
const zcMin = zc - VOX_OBJECT_CSIZE / 2;
return (zcMin >= 0) ? zcMin : zc;
});
const posMax = canvasSize / THREE_TILE_SIZE - 1;
const [xcMax, ycMax] = [xc, yc].map((zc) => {
if (zc > posMax) {
return zc - 1;
}
const zcMax = zc + VOX_OBJECT_CSIZE / 2 + 1;
return (zcMin <= posMax) ? zcMax : zc;
});
// Size Chunk
// 4 bytes SIZE
// 4 bytes size
// 4 bytes size children chunks (0)
// 4 * 3 (x, y, z) size
const sizeChunkSize = 4 * 3;
const sizeChunkLength = 4 + 4 + 4 + sizeChunkSize;
// Voxel Chunk
// 4 bytes XYZI
// 4 bytes size
// 4 bytes size children chunks (0)
// 4 bytes numVoxels
// 4 bytes (x, y, z, clr) for every voxel
let numVoxels = 0;
for (let j = yxcMin; j <= ycMax; j += 1) {
for (let i = xcMin; i <= xcMax; i += 1) {
const key = `${i}:${j}`;
const { buffer } = chunks.get(key);
numVoxels += calcChunkSize(buffer);
}
}
const xyziChunkSize = 4 + 4 * numVoxels;
const xyziChunkLength = 4 + 4 + 4 + xyziChunkSize;
// 4 bytes 'RGBA'
// 4 bytes size
// 4 bytes children chunks (0)
// 4 * 256 palette content (rgba * 256 colors)
const rgbaChunkSize = 4 * 256;
const rgbaChunkLength = 4 + 4 + 4 + rgbaChunkSize;
const rgbaPadding = [256, 256, 256, 256];
// Main Chunk
// 4 bytes MAIN
// 4 bytes size (0)
// 4 bythes size children chnks
const mainChildrenChunkSize = sizeChunkLength
+ xyziChunkLength + rgbaChunkLength;
const mainChunkLength = 4 + 4 + 4 + mainChildrenChunkSize;
// 4 bytes 'VOX '
// 4 bythes version number
const fileLength = 4 + 4 + mainChunkLength;
const voxFile = new ArrayBuffer(fileLength);
const voxFileView = new DataView(voxFile);
// TODO how to ascii and how to make less ugly
const charEncoder = new TextEncoder('utf-8');
let offset = 0;
voxFileView.setUint8(charEncoder('V')[0], offset++);
voxFileView.setUint8(charEncoder('O')[0], offset++);
voxFileView.setUint8(charEncoder('X')[0], offset++);
voxFileView.setUint8(charEncoder(' ')[0], offset++);
voxFileView.setUint32(150, offset);
offset += 4;
voxFileView.setUint8(charEncoder('M')[0], offset++);
voxFileView.setUint8(charEncoder('A')[0], offset++);
voxFileView.setUint8(charEncoder('I')[0], offset++);
voxFileView.setUint8(charEncoder('N')[0], offset++);
voxFileView.setUint32(0, offset);
offset += 4;
voxFileView.setUint32(mainChildrenChunkSize, offset);
offset += 4;
voxFileView.setUint8(charEncoder('S')[0], offset++);
voxFileView.setUint8(charEncoder('I')[0], offset++);
voxFileView.setUint8(charEncoder('Z')[0], offset++);
voxFileView.setUint8(charEncoder('E')[0], offset++);
voxFileView.setUint32(sizeChunkSize, offset);
offset += 4;
voxFileView.setUint32(0, offset);
offset += 4;
voxFileView.setUint32(VOX_OBJECT_SIZE, offset);
offset += 4;
voxFileView.setUint32(VOX_OBJECT_SIZE, offset);
offset += 4;
voxFileView.setUint32(THREE_CANVAS_HEIGHT, offset);
offset += 4;
voxFileView.setUint8(charEncoder('X')[0], offset++);
voxFileView.setUint8(charEncoder('Y')[0], offset++);
voxFileView.setUint8(charEncoder('Z')[0], offset++);
voxFileView.setUint8(charEncoder('I')[0], offset++);
voxFileView.setUint32(xyziChunkSize, offset);
offset += 4;
voxFileView.setUint32(0, offset);
offset += 4;
// TODO load voxels here
offset += xyziChunkSize;
voxFileView.setUint8(charEncoder('R')[0], offset++);
voxFileView.setUint8(charEncoder('G')[0], offset++);
voxFileView.setUint8(charEncoder('B')[0], offset++);
voxFileView.setUint8(charEncoder('A')[0], offset++);
voxFileView.setUint32(rgbaChunkSize, offset);
offset += 4;
voxFileView.setUint32(0, offset);
offset += 4;
// TODO load palette here, unused indices set to rgbaPadding
return voxFile;
// can then be saved from the UI with react-file-downloader
// aka js-file-doanloader
}
export default exportVox;

View File

@ -259,8 +259,7 @@ class Renderer {
return null;
}
// TODO use GC to dispose unused chunks
// eslint-disable-next-line class-methods-use-this
// TODO check if GC even works on 3D canvas
getAllChunks() {
return this.chunkLoader.getAllChunks();
}