Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7d3cce1c81 | ||
|
ac1505fa0d | ||
|
a1d0fa1aff | ||
|
1d051dbcc8 | ||
|
30848dc02a | ||
|
7be3b89159 | ||
|
6e81858e35 | ||
|
70d4df2d2d | ||
|
0f543d809d | ||
|
43962b1719 | ||
|
7b61bff346 | ||
|
0766f628a0 | ||
|
2754ecf3f6 | ||
|
c51c4d900c | ||
|
3729986623 | ||
|
803cc6c326 | ||
|
b7d6bce1a2 | ||
|
61b32b2b0d | ||
|
8232fe8098 | ||
|
926862c7a9 | ||
|
6e5de859f9 | ||
|
243595c39d | ||
|
16cc8290c9 | ||
|
87facde6eb | ||
|
06a915d3b7 | ||
|
a7838305ca | ||
|
00389813bf | ||
|
2fb092f835 | ||
|
b72de9b439 | ||
|
168874c091 | ||
|
255f9fb147 | ||
|
17e385b637 | ||
|
845b8a15a2 | ||
|
653bd14b9e | ||
|
4d77a58d17 | ||
|
9fae5a4336 | ||
|
bc1ee90b92 | ||
|
5aca2d2cda | ||
|
c6da004f6e | ||
|
48e884c4e6 | ||
|
d5ff00f834 | ||
|
882d8a476c | ||
|
9bb3e4fe9f | ||
|
dfe848381c | ||
|
4cf0a64cb1 | ||
|
544a2433e5 | ||
|
fbbc57ad52 | ||
|
f533f84fc4 | ||
|
8d2edcbc99 | ||
|
8df204e914 | ||
|
6856cab23e | ||
|
df2ea5b5b5 | ||
|
9a20c95391 | ||
|
35241260f2 | ||
|
a2da8d4aa2 | ||
|
73552e8ec0 | ||
|
58ca25db18 | ||
|
f2f4ee94a4 | ||
|
d4f2934dcf | ||
|
0ea18700c5 | ||
|
4e8f3680e5 | ||
|
66d9619b9b | ||
|
ec3743a4c9 | ||
|
2c3cee5c4a | ||
|
72cf3dc53b | ||
|
63d982b148 | ||
|
4aaf0a7f9d | ||
|
60cd73a455 | ||
|
fb64ba342f | ||
|
1864e5d446 | ||
|
f66d9069f1 | ||
|
b5c228154c | ||
|
33ce259926 |
@ -1,4 +1,4 @@
|
||||
name: Build
|
||||
name: Build-Core
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
@ -6,6 +6,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./Core
|
||||
run: npm ci
|
||||
- name: Run build
|
||||
working-directory: ./Core
|
||||
run: npm run build
|
16
.github/workflows/build_editor.js.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Build-Editor
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Core
|
||||
working-directory: ./Core
|
||||
run: npm ci
|
||||
- name: Install Editor
|
||||
working-directory: ./Editor
|
||||
run: npm ci
|
||||
- name: Run build
|
||||
working-directory: ./Editor
|
||||
run: npm run build
|
13
.github/workflows/build_sandbox.js.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Build-Sandbox
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./Sandbox
|
||||
run: npm ci
|
||||
- name: Run build
|
||||
working-directory: ./Sandbox
|
||||
run: npm run build
|
@ -1,4 +1,4 @@
|
||||
name: Tests
|
||||
name: Tests-Core
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
@ -6,6 +6,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./Core
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
working-directory: ./Core
|
||||
run: npm test
|
13
.github/workflows/tests_editor.js.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Tests-Editor
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./Editor
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
working-directory: ./Editor
|
||||
run: npm test
|
13
.github/workflows/tests_sandbox.js.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Tests-Sandbox
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
working-directory: ./Sandbox
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
working-directory: ./Sandbox
|
||||
run: npm test
|
39
.gitignore
vendored
@ -1,24 +1,17 @@
|
||||
/node_modules
|
||||
ObjToSchematic-win32-x64
|
||||
ObjToSchematic-linux-x64
|
||||
ObjToSchematic-darwin-x64
|
||||
/res/atlases/*.atlas
|
||||
/res/atlases/*.png
|
||||
!/res/atlases/vanilla.atlas
|
||||
!/res/atlases/vanilla.png
|
||||
/res/palettes/empty.palette
|
||||
/dist
|
||||
/dev
|
||||
/gen
|
||||
/tools/blocks
|
||||
/tools/models
|
||||
/tests/out
|
||||
/logs/
|
||||
/release/
|
||||
notes.txt
|
||||
# Common
|
||||
*.DS_Store
|
||||
.dependency-cruiser.js
|
||||
dependencygraph.svg
|
||||
.firebase
|
||||
/webpack/
|
||||
.firebaserc
|
||||
|
||||
# Core
|
||||
/Core/node_modules
|
||||
/Core/tests/out
|
||||
/Core/lib
|
||||
|
||||
# Editor
|
||||
/Editor/node_modules
|
||||
/Editor/.firebase
|
||||
/Editor/.firebaserc
|
||||
/Editor/build
|
||||
|
||||
# Sandbox
|
||||
/Sandbox/node_modules
|
||||
/Sandbox/lib
|
12
Core/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { OtS_ImporterFactory } from './src/importers/importers';
|
||||
import { OtS_Texture } from './src/ots_texture';
|
||||
import { OtS_VoxelMesh } from './src/ots_voxel_mesh';
|
||||
import { OtS_VoxelMesh_Converter } from './src/ots_voxel_mesh_converter';
|
||||
|
||||
export default {
|
||||
getImporter: OtS_ImporterFactory.GetImporter,
|
||||
texture: OtS_Texture,
|
||||
|
||||
voxelMeshConverter: OtS_VoxelMesh_Converter,
|
||||
voxelMesh: OtS_VoxelMesh,
|
||||
};
|
6506
Core/package-lock.json
generated
Normal file
27
Core/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ots-core",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest --config jestconfig.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@loaders.gl/core": "^3.4.14",
|
||||
"@loaders.gl/gltf": "^3.4.14",
|
||||
"pako": "^2.1.0",
|
||||
"prismarine-nbt": "^2.2.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/pako": "^2.0.1",
|
||||
"jest": "^29.7.0"
|
||||
}
|
||||
}
|
@ -1,158 +1,148 @@
|
||||
import { Atlas, TAtlasBlock } from './atlas';
|
||||
import { RGBA, RGBA_255, RGBAUtil } from './colour';
|
||||
import { AppMath } from './math';
|
||||
import { Palette } from './palette';
|
||||
import { AppTypes, TOptional } from './util';
|
||||
import { ASSERT } from './util/error_util';
|
||||
|
||||
export type TBlockCollection = {
|
||||
blocks: Map<AppTypes.TNamespacedBlockName, TAtlasBlock>,
|
||||
cache: Map<BigInt, TAtlasBlock>,
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export enum EFaceVisibility {
|
||||
None = 0,
|
||||
Up = 1 << 0,
|
||||
Down = 1 << 1,
|
||||
North = 1 << 2,
|
||||
East = 1 << 3,
|
||||
South = 1 << 4,
|
||||
West = 1 << 5,
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
/**
|
||||
* A new instance of AtlasPalette is created each time
|
||||
* a new voxel mesh is voxelised.
|
||||
*/
|
||||
export class AtlasPalette {
|
||||
private _atlas: Atlas;
|
||||
private _palette: Palette;
|
||||
|
||||
public constructor(atlas: Atlas, palette: Palette) {
|
||||
this._atlas = atlas;
|
||||
this._palette = palette;
|
||||
|
||||
this._palette.removeMissingAtlasBlocks(this._atlas);
|
||||
}
|
||||
|
||||
public createBlockCollection(blocksToExclude: AppTypes.TNamespacedBlockName[]): TBlockCollection {
|
||||
const blocksNamesToUse = this._palette.getBlocks();
|
||||
{
|
||||
// Remove excluded blocks
|
||||
for (const blockToExclude of blocksToExclude) {
|
||||
const index = blocksNamesToUse.indexOf(blockToExclude);
|
||||
if (index != -1) {
|
||||
blocksNamesToUse.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blocksToUse: TBlockCollection = {
|
||||
blocks: new Map(),
|
||||
cache: new Map(),
|
||||
};
|
||||
|
||||
const atlasBlocks = this._atlas.getBlocks();
|
||||
{
|
||||
// Only add block data for blocks in the palette
|
||||
atlasBlocks.forEach((atlasBlock, blockName) => {
|
||||
if (blocksNamesToUse.includes(blockName)) {
|
||||
blocksToUse.blocks.set(blockName, atlasBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ASSERT(blocksToUse.blocks.size >= 1, 'Must have at least one block cached');
|
||||
return blocksToUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a colour into a Minecraft block.
|
||||
* @param colour The colour that the returned block should match with.
|
||||
* @param resolution The colour accuracy, a uint8 from 1 to 255, inclusive.
|
||||
* @param blockToExclude A list of blocks that should not be used, this should be a subset of the palette blocks.
|
||||
* @returns
|
||||
*/
|
||||
public getBlock(colour: RGBA_255, blockCollection: TBlockCollection, faceVisibility: EFaceVisibility, errorWeight: number) {
|
||||
const colourHash = RGBAUtil.hash255(colour);
|
||||
const contextHash: BigInt = (BigInt(colourHash) << BigInt(6)) + BigInt(faceVisibility);
|
||||
|
||||
// If we've already calculated the block associated with this colour, return it.
|
||||
const cachedBlock = blockCollection.cache.get(contextHash);
|
||||
if (cachedBlock !== undefined) {
|
||||
return cachedBlock;
|
||||
}
|
||||
|
||||
// Find closest block in colour
|
||||
let minError = Infinity;
|
||||
let blockChoice: TOptional<TAtlasBlock>;
|
||||
{
|
||||
blockCollection.blocks.forEach((blockData) => {
|
||||
const context = AtlasPalette.getContextualFaceAverage(blockData, faceVisibility);
|
||||
const contextualBlockColour = faceVisibility !== EFaceVisibility.None ? context.colour : blockData.colour;
|
||||
const contextualStd = faceVisibility !== EFaceVisibility.None ? context.std : 0.0;
|
||||
const floatColour = RGBAUtil.fromRGBA255(colour);
|
||||
const rgbError = RGBAUtil.squaredDistance(floatColour, contextualBlockColour);
|
||||
const stdError = contextualStd;
|
||||
const totalError = AppMath.lerp(errorWeight, rgbError, stdError);
|
||||
if (totalError < minError) {
|
||||
minError = totalError;
|
||||
blockChoice = blockData;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (blockChoice !== undefined) {
|
||||
blockCollection.cache.set(contextHash, blockChoice);
|
||||
return blockChoice;
|
||||
}
|
||||
|
||||
ASSERT(false, 'Unreachable, always at least one possible block');
|
||||
}
|
||||
|
||||
public static getContextualFaceAverage(block: TAtlasBlock, faceVisibility: EFaceVisibility) {
|
||||
const averageColour: RGBA = { r: 0, g: 0, b: 0, a: 0 };
|
||||
let averageStd: number = 0.0; // Taking the average of a std is a bit naughty
|
||||
let count = 0;
|
||||
if (faceVisibility & EFaceVisibility.Up) {
|
||||
RGBAUtil.add(averageColour, block.faces.up.colour);
|
||||
averageStd += block.faces.up.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & EFaceVisibility.Down) {
|
||||
RGBAUtil.add(averageColour, block.faces.down.colour);
|
||||
averageStd += block.faces.down.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & EFaceVisibility.North) {
|
||||
RGBAUtil.add(averageColour, block.faces.north.colour);
|
||||
averageStd += block.faces.north.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & EFaceVisibility.East) {
|
||||
RGBAUtil.add(averageColour, block.faces.east.colour);
|
||||
averageStd += block.faces.east.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & EFaceVisibility.South) {
|
||||
RGBAUtil.add(averageColour, block.faces.south.colour);
|
||||
averageStd += block.faces.south.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & EFaceVisibility.West) {
|
||||
RGBAUtil.add(averageColour, block.faces.west.colour);
|
||||
averageStd += block.faces.west.std;
|
||||
++count;
|
||||
}
|
||||
averageColour.r /= count;
|
||||
averageColour.g /= count;
|
||||
averageColour.b /= count;
|
||||
averageColour.a /= count;
|
||||
return {
|
||||
colour: averageColour,
|
||||
std: averageStd / count,
|
||||
};
|
||||
}
|
||||
}
|
||||
import { Atlas, TAtlasBlock } from '../../Editor/src/atlas';
|
||||
import { RGBA, RGBA_255, RGBAUtil } from './colour';
|
||||
import { AppMath } from './math';
|
||||
import { OtS_FaceVisibility } from './ots_voxel_mesh_neighbourhood';
|
||||
import { AppTypes, TOptional } from './util';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { BlockPalette } from './util/type_util';
|
||||
|
||||
export type TBlockCollection = {
|
||||
blocks: Map<AppTypes.TNamespacedBlockName, TAtlasBlock>,
|
||||
cache: Map<BigInt, TAtlasBlock>,
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
/**
|
||||
* A new instance of AtlasPalette is created each time
|
||||
* a new voxel mesh is voxelised.
|
||||
*/
|
||||
export class AtlasPalette {
|
||||
private _atlas: Atlas;
|
||||
private _palette: BlockPalette;
|
||||
|
||||
public constructor(atlas: Atlas, palette: BlockPalette) {
|
||||
this._atlas = atlas;
|
||||
this._palette = palette;
|
||||
}
|
||||
|
||||
public createBlockCollection(blocksToExclude: AppTypes.TNamespacedBlockName[]): TBlockCollection {
|
||||
const blocksNamesToUse = Array.from(this._palette);
|
||||
|
||||
// Remove excluded blocks
|
||||
for (const blockToExclude of blocksToExclude) {
|
||||
const index = blocksNamesToUse.indexOf(blockToExclude);
|
||||
if (index != -1) {
|
||||
blocksNamesToUse.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const blocksToUse: TBlockCollection = {
|
||||
blocks: new Map(),
|
||||
cache: new Map(),
|
||||
};
|
||||
|
||||
const atlasBlocks = this._atlas.getBlocks();
|
||||
{
|
||||
// Only add block data for blocks in the palette
|
||||
atlasBlocks.forEach((atlasBlock, blockName) => {
|
||||
if (blocksNamesToUse.includes(blockName)) {
|
||||
blocksToUse.blocks.set(blockName, atlasBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ASSERT(blocksToUse.blocks.size >= 1, 'Must have at least one block cached');
|
||||
return blocksToUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a colour into a Minecraft block.
|
||||
* @param colour The colour that the returned block should match with.
|
||||
* @param resolution The colour accuracy, a uint8 from 1 to 255, inclusive.
|
||||
* @param blockToExclude A list of blocks that should not be used, this should be a subset of the palette blocks.
|
||||
* @returns
|
||||
*/
|
||||
public getBlock(colour: RGBA_255, blockCollection: TBlockCollection, faceVisibility: OtS_FaceVisibility, errorWeight: number) {
|
||||
const colourHash = RGBAUtil.hash255(colour);
|
||||
const contextHash: BigInt = (BigInt(colourHash) << BigInt(6)) + BigInt(faceVisibility);
|
||||
|
||||
// If we've already calculated the block associated with this colour, return it.
|
||||
const cachedBlock = blockCollection.cache.get(contextHash);
|
||||
if (cachedBlock !== undefined) {
|
||||
return cachedBlock;
|
||||
}
|
||||
|
||||
// Find closest block in colour
|
||||
let minError = Infinity;
|
||||
let blockChoice: TOptional<TAtlasBlock>;
|
||||
{
|
||||
blockCollection.blocks.forEach((blockData) => {
|
||||
const context = AtlasPalette.getContextualFaceAverage(blockData, faceVisibility);
|
||||
const contextualBlockColour = faceVisibility !== OtS_FaceVisibility.None ? context.colour : blockData.colour;
|
||||
const contextualStd = faceVisibility !== OtS_FaceVisibility.None ? context.std : 0.0;
|
||||
const floatColour = RGBAUtil.fromRGBA255(colour);
|
||||
const rgbError = RGBAUtil.squaredDistance(floatColour, contextualBlockColour);
|
||||
const stdError = contextualStd;
|
||||
const totalError = AppMath.lerp(errorWeight, rgbError, stdError);
|
||||
if (totalError < minError) {
|
||||
minError = totalError;
|
||||
blockChoice = blockData;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (blockChoice !== undefined) {
|
||||
blockCollection.cache.set(contextHash, blockChoice);
|
||||
return blockChoice;
|
||||
}
|
||||
|
||||
ASSERT(false, 'Unreachable, always at least one possible block');
|
||||
}
|
||||
|
||||
public static getContextualFaceAverage(block: TAtlasBlock, faceVisibility: OtS_FaceVisibility) {
|
||||
const averageColour: RGBA = { r: 0, g: 0, b: 0, a: 0 };
|
||||
let averageStd: number = 0.0; // Taking the average of a std is a bit naughty
|
||||
let count = 0;
|
||||
if (faceVisibility & OtS_FaceVisibility.Up) {
|
||||
RGBAUtil.add(averageColour, block.faces.up.colour);
|
||||
averageStd += block.faces.up.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & OtS_FaceVisibility.Down) {
|
||||
RGBAUtil.add(averageColour, block.faces.down.colour);
|
||||
averageStd += block.faces.down.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & OtS_FaceVisibility.North) {
|
||||
RGBAUtil.add(averageColour, block.faces.north.colour);
|
||||
averageStd += block.faces.north.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & OtS_FaceVisibility.East) {
|
||||
RGBAUtil.add(averageColour, block.faces.east.colour);
|
||||
averageStd += block.faces.east.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & OtS_FaceVisibility.South) {
|
||||
RGBAUtil.add(averageColour, block.faces.south.colour);
|
||||
averageStd += block.faces.south.std;
|
||||
++count;
|
||||
}
|
||||
if (faceVisibility & OtS_FaceVisibility.West) {
|
||||
RGBAUtil.add(averageColour, block.faces.west.colour);
|
||||
averageStd += block.faces.west.std;
|
||||
++count;
|
||||
}
|
||||
averageColour.r /= count;
|
||||
averageColour.g /= count;
|
||||
averageColour.b /= count;
|
||||
averageColour.a /= count;
|
||||
return {
|
||||
colour: averageColour,
|
||||
std: averageStd / count,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,23 +1,23 @@
|
||||
import { RGBA } from './colour';
|
||||
import { UV } from './util';
|
||||
|
||||
export interface TextureInfo {
|
||||
name: string
|
||||
texcoord: UV
|
||||
}
|
||||
|
||||
export interface FaceInfo {
|
||||
[face: string]: TextureInfo,
|
||||
up: TextureInfo,
|
||||
down: TextureInfo,
|
||||
north: TextureInfo,
|
||||
south: TextureInfo,
|
||||
east: TextureInfo,
|
||||
west: TextureInfo
|
||||
}
|
||||
|
||||
export interface BlockInfo {
|
||||
name: string;
|
||||
colour: RGBA;
|
||||
faces: FaceInfo
|
||||
}
|
||||
import { RGBA } from './colour';
|
||||
import { UV } from './util';
|
||||
|
||||
export interface TextureInfo {
|
||||
name: string
|
||||
texcoord: UV
|
||||
}
|
||||
|
||||
export interface FaceInfo {
|
||||
[face: string]: TextureInfo,
|
||||
up: TextureInfo,
|
||||
down: TextureInfo,
|
||||
north: TextureInfo,
|
||||
south: TextureInfo,
|
||||
east: TextureInfo,
|
||||
west: TextureInfo
|
||||
}
|
||||
|
||||
export interface BlockInfo {
|
||||
name: string;
|
||||
colour: RGBA;
|
||||
faces: FaceInfo
|
||||
}
|
@ -1,285 +1,267 @@
|
||||
import { Atlas, TAtlasBlock } from './atlas';
|
||||
import { AtlasPalette, EFaceVisibility } from './block_assigner';
|
||||
import { BlockInfo } from './block_atlas';
|
||||
import { ChunkedBufferGenerator, TBlockMeshBufferDescription } from './buffer';
|
||||
import { RGBA_255, RGBAUtil } from './colour';
|
||||
import { AppRuntimeConstants } from './constants';
|
||||
import { Ditherer } from './dither';
|
||||
import { BlockMeshLighting } from './lighting';
|
||||
import { LOC } from './localiser';
|
||||
import { Palette } from './palette';
|
||||
import { ProgressManager } from './progress';
|
||||
import { StatusHandler } from './status';
|
||||
import { ColourSpace, TOptional } from './util';
|
||||
import { AppError, ASSERT } from './util/error_util';
|
||||
import { LOGF } from './util/log_util';
|
||||
import { Vector3 } from './vector';
|
||||
import { Voxel, VoxelMesh } from './voxel_mesh';
|
||||
import { AssignParams } from './worker_types';
|
||||
|
||||
interface Block {
|
||||
voxel: Voxel;
|
||||
blockInfo: BlockInfo;
|
||||
}
|
||||
|
||||
interface GrassLikeBlock {
|
||||
hash: number;
|
||||
voxelColour: RGBA_255;
|
||||
errWeight: number;
|
||||
faceVisibility: EFaceVisibility;
|
||||
}
|
||||
|
||||
export type FallableBehaviour = 'replace-falling' | 'replace-fallable' | 'place-string' | 'do-nothing';
|
||||
|
||||
export interface BlockMeshParams {
|
||||
textureAtlas: Atlas,
|
||||
blockPalette: Palette,
|
||||
colourSpace: ColourSpace,
|
||||
fallable: FallableBehaviour,
|
||||
}
|
||||
|
||||
export class BlockMesh {
|
||||
private _blocksUsed: Set<string>;
|
||||
private _blocks: Map<number, Block>;
|
||||
//private _blocks: Block[];
|
||||
private _voxelMesh: VoxelMesh;
|
||||
private _atlas: Atlas;
|
||||
private _lighting: BlockMeshLighting;
|
||||
|
||||
public static createFromVoxelMesh(voxelMesh: VoxelMesh, blockMeshParams: AssignParams.Input) {
|
||||
const blockMesh = new BlockMesh(voxelMesh);
|
||||
blockMesh._assignBlocks(blockMeshParams);
|
||||
|
||||
//blockMesh._calculateLighting(blockMeshParams.lightThreshold);
|
||||
if (blockMeshParams.calculateLighting) {
|
||||
blockMesh._lighting.init();
|
||||
blockMesh._lighting.addSunLightValues();
|
||||
blockMesh._lighting.addEmissiveBlocks();
|
||||
blockMesh._lighting.addLightToDarkness(blockMeshParams.lightThreshold);
|
||||
blockMesh._lighting.dumpInfo();
|
||||
}
|
||||
|
||||
return blockMesh;
|
||||
}
|
||||
|
||||
private constructor(voxelMesh: VoxelMesh) {
|
||||
this._blocksUsed = new Set();
|
||||
this._blocks = new Map();
|
||||
this._voxelMesh = voxelMesh;
|
||||
this._atlas = Atlas.getVanillaAtlas()!;
|
||||
this._lighting = new BlockMeshLighting(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Before we turn a voxel into a block we have the opportunity to alter the voxel's colour.
|
||||
* This is where the colour accuracy bands colours together and where dithering is calculated.
|
||||
*/
|
||||
private _getFinalVoxelColour(voxel: Voxel, blockMeshParams: AssignParams.Input) {
|
||||
const voxelColour = RGBAUtil.copy(voxel.colour);
|
||||
|
||||
const binnedColour = RGBAUtil.bin(voxelColour, blockMeshParams.resolution);
|
||||
|
||||
const ditheredColour: RGBA_255 = RGBAUtil.copy255(binnedColour);
|
||||
switch (blockMeshParams.dithering) {
|
||||
case 'off': {
|
||||
break;
|
||||
}
|
||||
case 'random': {
|
||||
Ditherer.ditherRandom(ditheredColour, blockMeshParams.ditheringMagnitude);
|
||||
break;
|
||||
}
|
||||
case 'ordered': {
|
||||
Ditherer.ditherOrdered(ditheredColour, voxel.position, blockMeshParams.ditheringMagnitude);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ditheredColour;
|
||||
}
|
||||
|
||||
private _assignBlocks(blockMeshParams: AssignParams.Input) {
|
||||
const atlas = Atlas.load(blockMeshParams.textureAtlas);
|
||||
ASSERT(atlas !== undefined, 'Could not load atlas');
|
||||
this._atlas = atlas;
|
||||
|
||||
const palette = Palette.create();
|
||||
palette.add(blockMeshParams.blockPalette);
|
||||
ASSERT(palette !== undefined, 'Could not load palette');
|
||||
|
||||
const atlasPalette = new AtlasPalette(atlas, palette);
|
||||
const allBlockCollection = atlasPalette.createBlockCollection([]);
|
||||
const nonFallableBlockCollection = atlasPalette.createBlockCollection(Array.from(AppRuntimeConstants.Get.FALLABLE_BLOCKS));
|
||||
const grassLikeBlocksBuffer: GrassLikeBlock[] = [];
|
||||
|
||||
let countFalling = 0;
|
||||
const taskHandle = ProgressManager.Get.start('Assigning');
|
||||
const voxels = this._voxelMesh.getVoxels();
|
||||
for (let voxelIndex = 0; voxelIndex < voxels.length; ++voxelIndex) {
|
||||
ProgressManager.Get.progress(taskHandle, voxelIndex / voxels.length);
|
||||
|
||||
// Convert the voxel into a block
|
||||
const voxel = voxels[voxelIndex];
|
||||
const voxelColour = this._getFinalVoxelColour(voxel, blockMeshParams);
|
||||
const faceVisibility = blockMeshParams.contextualAveraging ?
|
||||
this._voxelMesh.getFaceVisibility(voxel.position) :
|
||||
VoxelMesh.getFullFaceVisibility();
|
||||
let block = atlasPalette.getBlock(voxelColour, allBlockCollection, faceVisibility, blockMeshParams.errorWeight);
|
||||
|
||||
// Check that this block meets the fallable behaviour, we may need
|
||||
// to choose a different block if the current one doesn't meet the requirements
|
||||
const isBlockFallable = AppRuntimeConstants.Get.FALLABLE_BLOCKS.has(block.name);
|
||||
const isBlockSupported = this._voxelMesh.isVoxelAt(Vector3.add(voxel.position, new Vector3(0, -1, 0)));
|
||||
|
||||
if (isBlockFallable && !isBlockSupported) {
|
||||
++countFalling;
|
||||
}
|
||||
|
||||
const shouldReplaceBlock =
|
||||
(blockMeshParams.fallable === 'replace-fallable' && isBlockFallable) ||
|
||||
(blockMeshParams.fallable === 'replace-falling' && isBlockFallable && !isBlockSupported);
|
||||
|
||||
if (shouldReplaceBlock) {
|
||||
block = atlasPalette.getBlock(voxelColour, nonFallableBlockCollection, faceVisibility, blockMeshParams.errorWeight);
|
||||
}
|
||||
|
||||
if (AppRuntimeConstants.Get.GRASS_LIKE_BLOCKS.has(block.name)) {
|
||||
grassLikeBlocksBuffer.push({
|
||||
hash: voxel.position.hash(),
|
||||
voxelColour: voxelColour,
|
||||
errWeight: blockMeshParams.errorWeight,
|
||||
faceVisibility: faceVisibility,
|
||||
});
|
||||
}
|
||||
|
||||
this._blocks.set(voxel.position.hash(), {
|
||||
voxel: voxel,
|
||||
blockInfo: block,
|
||||
});
|
||||
this._blocksUsed.add(block.name);
|
||||
}
|
||||
|
||||
if (grassLikeBlocksBuffer.length > 0) {
|
||||
const nonGrassLikeBlockCollection = atlasPalette.createBlockCollection(Array.from(AppRuntimeConstants.Get.GRASS_LIKE_BLOCKS));
|
||||
for (let index=0; index < grassLikeBlocksBuffer.length; index++) {
|
||||
ProgressManager.Get.progress(taskHandle, index / grassLikeBlocksBuffer.length);
|
||||
const examined = grassLikeBlocksBuffer[index];
|
||||
const examinedBlock = this._blocks.get(examined.hash);
|
||||
ASSERT(examinedBlock, 'Missing examined block');
|
||||
|
||||
const topBlockPosition = Vector3.add(examinedBlock.voxel.position, new Vector3(0, 1, 0));
|
||||
const topBlock = this._blocks.get(topBlockPosition.hash());
|
||||
if (topBlock !== undefined) {
|
||||
if (!AppRuntimeConstants.Get.TRANSPARENT_BLOCKS.has(topBlock.blockInfo.name)) {
|
||||
const block = atlasPalette.getBlock(examined.voxelColour, nonGrassLikeBlockCollection, examined.faceVisibility, examined.errWeight);
|
||||
examinedBlock.blockInfo = block;
|
||||
this._blocks.set(examined.hash, examinedBlock);
|
||||
this._blocksUsed.add(block.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProgressManager.Get.end(taskHandle);
|
||||
|
||||
if (blockMeshParams.fallable === 'do-nothing' && countFalling > 0) {
|
||||
StatusHandler.warning(LOC('assign.falling_blocks', { count: countFalling }));
|
||||
}
|
||||
}
|
||||
|
||||
// Face order: ['north', 'south', 'up', 'down', 'east', 'west']
|
||||
public getBlockLighting(position: Vector3) {
|
||||
// TODO: Shouldn't only use sunlight value, take max of either
|
||||
return [
|
||||
this._lighting.getMaxLightLevel(new Vector3(1, 0, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(-1, 0, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, 1, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, -1, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, 0, 1).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, 0, -1).add(position)),
|
||||
];
|
||||
}
|
||||
|
||||
public setEmissiveBlock(pos: Vector3): boolean {
|
||||
const voxel = this._voxelMesh.getVoxelAt(pos);
|
||||
ASSERT(voxel !== undefined, 'Missing voxel');
|
||||
const minError = Infinity;
|
||||
let bestBlock: TAtlasBlock | undefined;
|
||||
AppRuntimeConstants.Get.EMISSIVE_BLOCKS.forEach((emissiveBlockName) => {
|
||||
const emissiveBlockData = this._atlas.getBlocks().get(emissiveBlockName);
|
||||
if (emissiveBlockData) {
|
||||
const error = RGBAUtil.squaredDistance(emissiveBlockData.colour, voxel.colour);
|
||||
if (error < minError) {
|
||||
bestBlock = emissiveBlockData;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (bestBlock !== undefined) {
|
||||
const blockIndex = 0; //this._voxelMesh.getVoxelIndex(pos);
|
||||
ASSERT(blockIndex !== undefined, 'Setting emissive block of block that doesn\'t exist');
|
||||
|
||||
const block = this._blocks.get(pos.hash());
|
||||
ASSERT(block !== undefined);
|
||||
|
||||
block.blockInfo = bestBlock;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new AppError(LOC('assign.block_palette_missing_light_blocks'));
|
||||
}
|
||||
|
||||
public getBlockAt(pos: Vector3): TOptional<Block> {
|
||||
return this._blocks.get(pos.hash());
|
||||
}
|
||||
|
||||
public getBlocks(): Block[] {
|
||||
return Array.from(this._blocks.values());
|
||||
}
|
||||
|
||||
public getBlockPalette() {
|
||||
return Array.from(this._blocksUsed);
|
||||
}
|
||||
|
||||
public getVoxelMesh() {
|
||||
ASSERT(this._voxelMesh !== undefined, 'Block mesh has no voxel mesh');
|
||||
return this._voxelMesh;
|
||||
}
|
||||
|
||||
public getAtlas() {
|
||||
return this._atlas;
|
||||
}
|
||||
|
||||
public isEmissiveBlock(block: Block) {
|
||||
return AppRuntimeConstants.Get.EMISSIVE_BLOCKS.has(block.blockInfo.name);
|
||||
}
|
||||
|
||||
public isTransparentBlock(block: Block) {
|
||||
return AppRuntimeConstants.Get.TRANSPARENT_BLOCKS.has(block.blockInfo.name);
|
||||
}
|
||||
|
||||
/*
|
||||
private _buffer?: TBlockMeshBufferDescription;
|
||||
public getBuffer(): TBlockMeshBufferDescription {
|
||||
//ASSERT(this._renderParams, 'Called BlockMesh.getBuffer() without setting render params');
|
||||
if (this._buffer === undefined) {
|
||||
this._buffer = BufferGenerator.fromBlockMesh(this);
|
||||
//this._recreateBuffer = false;
|
||||
}
|
||||
return this._buffer;
|
||||
}
|
||||
*/
|
||||
|
||||
private _bufferChunks: Array<TBlockMeshBufferDescription & { moreBlocksToBuffer: boolean, progress: number }> = [];
|
||||
public getChunkedBuffer(chunkIndex: number): TBlockMeshBufferDescription & { moreBlocksToBuffer: boolean, progress: number } {
|
||||
if (this._bufferChunks[chunkIndex] === undefined) {
|
||||
LOGF(`[BlockMesh]: getChunkedBuffer: ci: ${chunkIndex} not cached`);
|
||||
this._bufferChunks[chunkIndex] = ChunkedBufferGenerator.fromBlockMesh(this, chunkIndex);
|
||||
} else {
|
||||
LOGF(`[BlockMesh]: getChunkedBuffer: ci: ${chunkIndex} not cached`);
|
||||
}
|
||||
return this._bufferChunks[chunkIndex];
|
||||
}
|
||||
|
||||
public getAllChunkedBuffers() {
|
||||
return this._bufferChunks;
|
||||
}
|
||||
}
|
||||
import { Atlas, TAtlasBlock } from '../../Editor/src/atlas';
|
||||
import { AtlasPalette } from './block_assigner';
|
||||
import { BlockInfo } from './block_atlas';
|
||||
import { RGBA_255, RGBAUtil } from './colour';
|
||||
import { AppRuntimeConstants } from './constants';
|
||||
import { Ditherer } from './dither';
|
||||
import { BlockMeshLighting } from './lighting';
|
||||
import { TOptional } from './util';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
import { BlockPalette, TDithering } from './util/type_util';
|
||||
import { OtS_Voxel, OtS_VoxelMesh } from './ots_voxel_mesh';
|
||||
import { OtS_FaceVisibility, OtS_VoxelMesh_Neighbourhood } from './ots_voxel_mesh_neighbourhood';
|
||||
|
||||
export interface Block {
|
||||
voxel: OtS_Voxel;
|
||||
blockInfo: BlockInfo;
|
||||
}
|
||||
|
||||
interface GrassLikeBlock {
|
||||
hash: number;
|
||||
voxelColour: RGBA_255;
|
||||
errWeight: number;
|
||||
faceVisibility: OtS_FaceVisibility;
|
||||
}
|
||||
|
||||
export type FallableBehaviour = 'replace-falling' | 'replace-fallable' | 'place-string' | 'do-nothing';
|
||||
|
||||
export interface BlockMeshParams {
|
||||
blockPalette: BlockPalette,
|
||||
dithering: TDithering,
|
||||
ditheringMagnitude: number,
|
||||
fallable: FallableBehaviour,
|
||||
resolution: RGBAUtil.TColourAccuracy,
|
||||
calculateLighting: boolean,
|
||||
lightThreshold: number,
|
||||
contextualAveraging: boolean,
|
||||
errorWeight: number,
|
||||
atlasJSON: any,
|
||||
}
|
||||
|
||||
export type TAssignBlocksWarning =
|
||||
| { type: 'falling-blocks', count: number };
|
||||
|
||||
export class BlockMesh {
|
||||
private _blocksUsed: Set<string>;
|
||||
private _blocks: Map<number, Block>;
|
||||
private _voxelMesh: OtS_VoxelMesh;
|
||||
private _lighting: BlockMeshLighting;
|
||||
private _atlas?: Atlas;
|
||||
|
||||
public static createFromVoxelMesh(voxelMesh: OtS_VoxelMesh, blockMeshParams: BlockMeshParams) {
|
||||
const blockMesh = new BlockMesh(voxelMesh);
|
||||
|
||||
const atlas = Atlas.load(blockMeshParams.atlasJSON);
|
||||
blockMesh.setAtlas(atlas);
|
||||
|
||||
const warn = blockMesh._assignBlocks(blockMeshParams);
|
||||
|
||||
//blockMesh._calculateLighting(blockMeshParams.lightThreshold);
|
||||
if (blockMeshParams.calculateLighting) {
|
||||
blockMesh._lighting.init();
|
||||
blockMesh._lighting.addSunLightValues();
|
||||
blockMesh._lighting.addEmissiveBlocks();
|
||||
blockMesh._lighting.addLightToDarkness(blockMeshParams.lightThreshold);
|
||||
blockMesh._lighting.dumpInfo();
|
||||
}
|
||||
|
||||
return { blockMesh: blockMesh, warnings: warn };
|
||||
}
|
||||
|
||||
private constructor(voxelMesh: OtS_VoxelMesh) {
|
||||
this._blocksUsed = new Set();
|
||||
this._blocks = new Map();
|
||||
this._voxelMesh = voxelMesh;
|
||||
this._lighting = new BlockMeshLighting(this);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: Atlas) {
|
||||
this._atlas = atlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before we turn a voxel into a block we have the opportunity to alter the voxel's colour.
|
||||
* This is where the colour accuracy bands colours together and where dithering is calculated.
|
||||
*/
|
||||
private _getFinalVoxelColour(voxel: OtS_Voxel, blockMeshParams: BlockMeshParams) {
|
||||
const voxelColour = RGBAUtil.copy(voxel.colour);
|
||||
|
||||
const binnedColour = RGBAUtil.bin(voxelColour, blockMeshParams.resolution);
|
||||
|
||||
const ditheredColour: RGBA_255 = RGBAUtil.copy255(binnedColour);
|
||||
switch (blockMeshParams.dithering) {
|
||||
case 'off': {
|
||||
break;
|
||||
}
|
||||
case 'random': {
|
||||
Ditherer.ditherRandom(ditheredColour, blockMeshParams.ditheringMagnitude);
|
||||
break;
|
||||
}
|
||||
case 'ordered': {
|
||||
Ditherer.ditherOrdered(ditheredColour, voxel.position, blockMeshParams.ditheringMagnitude);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ditheredColour;
|
||||
}
|
||||
|
||||
private _assignBlocks(blockMeshParams: BlockMeshParams): (null | TAssignBlocksWarning) {
|
||||
ASSERT(this._atlas !== undefined, 'No atlas loaded');
|
||||
|
||||
const atlasPalette = new AtlasPalette(this._atlas, blockMeshParams.blockPalette);
|
||||
const allBlockCollection = atlasPalette.createBlockCollection([]);
|
||||
const nonFallableBlockCollection = atlasPalette.createBlockCollection(Array.from(AppRuntimeConstants.Get.FALLABLE_BLOCKS));
|
||||
const grassLikeBlocksBuffer: GrassLikeBlock[] = [];
|
||||
|
||||
const faceVisibilityCache = new OtS_VoxelMesh_Neighbourhood();
|
||||
faceVisibilityCache.process(this._voxelMesh, 'cardinal');
|
||||
|
||||
let countFalling = 0;
|
||||
// TODO: ProgressRework
|
||||
//const taskHandle = ProgressManager.Get.start('Assigning');
|
||||
const voxels = Array.from(this._voxelMesh.getVoxels());
|
||||
for (let voxelIndex = 0; voxelIndex < voxels.length; ++voxelIndex) {
|
||||
//ProgressManager.Get.progress(taskHandle, voxelIndex / voxels.length);
|
||||
|
||||
// Convert the voxel into a block
|
||||
const voxel = voxels[voxelIndex];
|
||||
const voxelColour = this._getFinalVoxelColour(voxel, blockMeshParams);
|
||||
const faceVisibility = blockMeshParams.contextualAveraging
|
||||
? faceVisibilityCache.getFaceVisibility(voxel.position.x, voxel.position.y, voxel.position.z)
|
||||
: OtS_FaceVisibility.Full;
|
||||
ASSERT(faceVisibility !== null, 'Neighbourhood cache processed with wrong mode');
|
||||
let block = atlasPalette.getBlock(voxelColour, allBlockCollection, faceVisibility, blockMeshParams.errorWeight);
|
||||
|
||||
// Check that this block meets the fallable behaviour, we may need
|
||||
// to choose a different block if the current one doesn't meet the requirements
|
||||
const isBlockFallable = AppRuntimeConstants.Get.FALLABLE_BLOCKS.has(block.name);
|
||||
const isBlockSupported = this._voxelMesh.isVoxelAt(voxel.position.x, voxel.position.y - 1, voxel.position.z);
|
||||
|
||||
if (isBlockFallable && !isBlockSupported) {
|
||||
++countFalling;
|
||||
}
|
||||
|
||||
const shouldReplaceBlock =
|
||||
(blockMeshParams.fallable === 'replace-fallable' && isBlockFallable) ||
|
||||
(blockMeshParams.fallable === 'replace-falling' && isBlockFallable && !isBlockSupported);
|
||||
|
||||
if (shouldReplaceBlock) {
|
||||
block = atlasPalette.getBlock(voxelColour, nonFallableBlockCollection, faceVisibility, blockMeshParams.errorWeight);
|
||||
}
|
||||
|
||||
if (AppRuntimeConstants.Get.GRASS_LIKE_BLOCKS.has(block.name)) {
|
||||
grassLikeBlocksBuffer.push({
|
||||
hash: voxel.position.hash(),
|
||||
voxelColour: voxelColour,
|
||||
errWeight: blockMeshParams.errorWeight,
|
||||
faceVisibility: faceVisibility,
|
||||
});
|
||||
}
|
||||
|
||||
this._blocks.set(voxel.position.hash(), {
|
||||
voxel: voxel,
|
||||
blockInfo: block,
|
||||
});
|
||||
this._blocksUsed.add(block.name);
|
||||
}
|
||||
|
||||
if (grassLikeBlocksBuffer.length > 0) {
|
||||
const nonGrassLikeBlockCollection = atlasPalette.createBlockCollection(Array.from(AppRuntimeConstants.Get.GRASS_LIKE_BLOCKS));
|
||||
for (let index=0; index < grassLikeBlocksBuffer.length; index++) {
|
||||
//ProgressManager.Get.progress(taskHandle, index / grassLikeBlocksBuffer.length);
|
||||
const examined = grassLikeBlocksBuffer[index];
|
||||
const examinedBlock = this._blocks.get(examined.hash);
|
||||
ASSERT(examinedBlock, 'Missing examined block');
|
||||
|
||||
const topBlockPosition = Vector3.add(examinedBlock.voxel.position, new Vector3(0, 1, 0));
|
||||
const topBlock = this._blocks.get(topBlockPosition.hash());
|
||||
if (topBlock !== undefined) {
|
||||
if (!AppRuntimeConstants.Get.TRANSPARENT_BLOCKS.has(topBlock.blockInfo.name)) {
|
||||
const block = atlasPalette.getBlock(examined.voxelColour, nonGrassLikeBlockCollection, examined.faceVisibility, examined.errWeight);
|
||||
examinedBlock.blockInfo = block;
|
||||
this._blocks.set(examined.hash, examinedBlock);
|
||||
this._blocksUsed.add(block.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//ProgressManager.Get.end(taskHandle);
|
||||
|
||||
if (blockMeshParams.fallable === 'do-nothing' && countFalling > 0) {
|
||||
return { type: 'falling-blocks', count: countFalling }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Face order: ['north', 'south', 'up', 'down', 'east', 'west']
|
||||
public getBlockLighting(position: Vector3) {
|
||||
// TODO: Shouldn't only use sunlight value, take max of either
|
||||
return [
|
||||
this._lighting.getMaxLightLevel(new Vector3(1, 0, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(-1, 0, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, 1, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, -1, 0).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, 0, 1).add(position)),
|
||||
this._lighting.getMaxLightLevel(new Vector3(0, 0, -1).add(position)),
|
||||
];
|
||||
}
|
||||
|
||||
public setEmissiveBlock(pos: Vector3): boolean {
|
||||
ASSERT(this._atlas, 'No atlas loaded');
|
||||
|
||||
const voxel = this._voxelMesh.getVoxelAt(pos.x, pos.y, pos.z);
|
||||
ASSERT(voxel !== null, 'Missing voxel');
|
||||
const minError = Infinity;
|
||||
let bestBlock: TAtlasBlock | undefined;
|
||||
AppRuntimeConstants.Get.EMISSIVE_BLOCKS.forEach((emissiveBlockName) => {
|
||||
const emissiveBlockData = this._atlas!.getBlocks().get(emissiveBlockName);
|
||||
if (emissiveBlockData) {
|
||||
const error = RGBAUtil.squaredDistance(emissiveBlockData.colour, voxel.colour);
|
||||
if (error < minError) {
|
||||
bestBlock = emissiveBlockData;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (bestBlock !== undefined) {
|
||||
const blockIndex = 0; //this._voxelMesh.getVoxelIndex(pos);
|
||||
ASSERT(blockIndex !== undefined, 'Setting emissive block of block that doesn\'t exist');
|
||||
|
||||
const block = this._blocks.get(pos.hash());
|
||||
ASSERT(block !== undefined);
|
||||
|
||||
block.blockInfo = bestBlock;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getBlockAt(pos: Vector3): TOptional<Block> {
|
||||
return this._blocks.get(pos.hash());
|
||||
}
|
||||
|
||||
public getBlocks(): Block[] {
|
||||
return Array.from(this._blocks.values());
|
||||
}
|
||||
|
||||
public getBlockPalette() {
|
||||
return Array.from(this._blocksUsed);
|
||||
}
|
||||
|
||||
public getVoxelMesh() {
|
||||
ASSERT(this._voxelMesh !== undefined, 'Block mesh has no voxel mesh');
|
||||
return this._voxelMesh;
|
||||
}
|
||||
|
||||
public isEmissiveBlock(block: Block) {
|
||||
return AppRuntimeConstants.Get.EMISSIVE_BLOCKS.has(block.blockInfo.name);
|
||||
}
|
||||
|
||||
public isTransparentBlock(block: Block) {
|
||||
return AppRuntimeConstants.Get.TRANSPARENT_BLOCKS.has(block.blockInfo.name);
|
||||
}
|
||||
}
|
23
Core/src/block_registry.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { RGBA } from "./colour"
|
||||
|
||||
export type OtS_BlockData = {
|
||||
north: RGBA,
|
||||
south: RGBA,
|
||||
up: RGBA,
|
||||
down: RGBA,
|
||||
east: RGBA,
|
||||
west: RGBA,
|
||||
};
|
||||
|
||||
export class OtS_BlockRegistry {
|
||||
private _blocks: Map<string, OtS_BlockData>;
|
||||
|
||||
public constructor() {
|
||||
this._blocks = new Map();
|
||||
}
|
||||
|
||||
public register(blockName: string, blockData: OtS_BlockData) {
|
||||
this._blocks.set(blockName, blockData);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -22,8 +22,7 @@ export class Bounds {
|
||||
this._max = Vector3.max(this._max, volume._max);
|
||||
}
|
||||
|
||||
// TODO: rename to `createInfinitesimalBounds`
|
||||
public static getInfiniteBounds() {
|
||||
public static getEmptyBounds() {
|
||||
return new Bounds(
|
||||
new Vector3(Infinity, Infinity, Infinity),
|
||||
new Vector3(-Infinity, -Infinity, -Infinity),
|
||||
@ -48,4 +47,11 @@ export class Bounds {
|
||||
public getDimensions() {
|
||||
return Vector3.sub(this._max, this._min);
|
||||
}
|
||||
|
||||
public copy() {
|
||||
return new Bounds(
|
||||
this._min.copy(),
|
||||
this._max.copy(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,201 +1,255 @@
|
||||
import { AppConfig } from './config';
|
||||
import { TBrand } from './util/type_util';
|
||||
|
||||
const hsv_rgb = require('hsv-rgb');
|
||||
|
||||
export type RGBA = {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number
|
||||
}
|
||||
|
||||
export type RGBA_255 = TBrand<RGBA, '255'>;
|
||||
|
||||
export namespace RGBAUtil {
|
||||
export function toString(a: RGBA) {
|
||||
return `(${a.r}, ${a.g}, ${a.b}, ${a.a})`;
|
||||
}
|
||||
|
||||
export function randomPretty(): RGBA {
|
||||
const hue = Math.random() * 360;
|
||||
const sat = 65;
|
||||
const val = 85;
|
||||
|
||||
const rgb: number[] = hsv_rgb(hue, sat, val);
|
||||
|
||||
return {
|
||||
r: rgb[0] / 255,
|
||||
g: rgb[1] / 255,
|
||||
b: rgb[2] / 255,
|
||||
a: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
export function random(): RGBA {
|
||||
return {
|
||||
r: Math.random(),
|
||||
g: Math.random(),
|
||||
b: Math.random(),
|
||||
a: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
export function toHexString(a: RGBA) {
|
||||
const r = Math.floor(255 * a.r).toString(16).padStart(2, '0');
|
||||
const g = Math.floor(255 * a.g).toString(16).padStart(2, '0');
|
||||
const b = Math.floor(255 * a.b).toString(16).padStart(2, '0');
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
|
||||
export function fromHexString(str: string) {
|
||||
return {
|
||||
r: parseInt(str.substring(1, 3), 16) / 255,
|
||||
g: parseInt(str.substring(3, 5), 16) / 255,
|
||||
b: parseInt(str.substring(5, 7), 16) / 255,
|
||||
a: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
export function toUint8String(a: RGBA) {
|
||||
return `(${Math.floor(255 * a.r)}, ${Math.floor(255 * a.g)}, ${Math.floor(255 * a.b)}, ${Math.floor(255 * a.a)})`;
|
||||
}
|
||||
|
||||
export function toRGBA255(c: RGBA): RGBA_255 {
|
||||
const out: RGBA = {
|
||||
r: c.r * 255,
|
||||
g: c.r * 255,
|
||||
b: c.r * 255,
|
||||
a: c.r * 255,
|
||||
};
|
||||
return out as RGBA_255;
|
||||
}
|
||||
|
||||
export function fromRGBA255(c: RGBA_255): RGBA {
|
||||
const out: RGBA = {
|
||||
r: c.r / 255,
|
||||
g: c.g / 255,
|
||||
b: c.b / 255,
|
||||
a: c.a / 255,
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
export function add(a: RGBA, b: RGBA) {
|
||||
a.r += b.r;
|
||||
a.g += b.g;
|
||||
a.b += b.b;
|
||||
a.a += b.a;
|
||||
}
|
||||
|
||||
export function lerp(a: RGBA, b: RGBA, alpha: number) {
|
||||
return {
|
||||
r: a.r * (1 - alpha) + b.r * alpha,
|
||||
g: a.g * (1 - alpha) + b.g * alpha,
|
||||
b: a.b * (1 - alpha) + b.b * alpha,
|
||||
a: a.a * (1 - alpha) + b.a * alpha,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Note this is a very naive approach to averaging a colour
|
||||
*/
|
||||
export function average(...colours: RGBA[]) {
|
||||
const avg = { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
for (let i = 0; i < colours.length; ++i) {
|
||||
avg.r += colours[i].r;
|
||||
avg.g += colours[i].g;
|
||||
avg.b += colours[i].b;
|
||||
avg.a += colours[i].a;
|
||||
}
|
||||
avg.r /= colours.length;
|
||||
avg.g /= colours.length;
|
||||
avg.b /= colours.length;
|
||||
avg.a /= colours.length;
|
||||
return avg;
|
||||
}
|
||||
|
||||
export function squaredDistance(a: RGBA, b: RGBA) {
|
||||
let squaredDistance = 0.0;
|
||||
squaredDistance += (a.r - b.r) * (a.r - b.r);
|
||||
squaredDistance += (a.g - b.g) * (a.g - b.g);
|
||||
squaredDistance += (a.b - b.b) * (a.b - b.b);
|
||||
squaredDistance += (a.a - b.a) * (a.a - b.a) * AppConfig.Get.ALPHA_BIAS;
|
||||
return squaredDistance;
|
||||
}
|
||||
|
||||
export function copy(a: RGBA): RGBA {
|
||||
return {
|
||||
r: a.r,
|
||||
g: a.g,
|
||||
b: a.b,
|
||||
a: a.a,
|
||||
};
|
||||
}
|
||||
|
||||
export function copy255(a: RGBA_255): RGBA_255 {
|
||||
return {
|
||||
r: a.r,
|
||||
g: a.g,
|
||||
b: a.b,
|
||||
a: a.a,
|
||||
} as RGBA_255;
|
||||
}
|
||||
|
||||
export function toArray(a: RGBA): number[] {
|
||||
return [a.r, a.g, a.b, a.a];
|
||||
}
|
||||
|
||||
export function bin(col: RGBA, resolution: TColourAccuracy) {
|
||||
const binnedColour: RGBA = {
|
||||
r: Math.floor(Math.floor(col.r * resolution) * (255 / resolution)),
|
||||
g: Math.floor(Math.floor(col.g * resolution) * (255 / resolution)),
|
||||
b: Math.floor(Math.floor(col.b * resolution) * (255 / resolution)),
|
||||
a: Math.floor(Math.ceil(col.a * resolution) * (255 / resolution)),
|
||||
};
|
||||
|
||||
return binnedColour as RGBA_255;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a colour as a single number.
|
||||
* Note this will bin colours together.
|
||||
* @param col The colour to hash.
|
||||
* @param resolution An uint8, the larger the more accurate the hash.
|
||||
*/
|
||||
export function hash(col: RGBA, resolution: TColourAccuracy): number {
|
||||
const r = Math.floor(col.r * resolution);
|
||||
const g = Math.floor(col.g * resolution);
|
||||
const b = Math.floor(col.b * resolution);
|
||||
const a = Math.floor(col.a * resolution);
|
||||
|
||||
let hash = r;
|
||||
hash = (hash << 8) + g;
|
||||
hash = (hash << 8) + b;
|
||||
hash = (hash << 8) + a;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function hash255(col: RGBA_255) {
|
||||
let hash = col.r;
|
||||
hash = (hash << 8) + col.g;
|
||||
hash = (hash << 8) + col.b;
|
||||
hash = (hash << 8) + col.a;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export type TColourAccuracy = number;
|
||||
}
|
||||
|
||||
export namespace RGBAColours {
|
||||
export const RED: RGBA = { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
export const GREEN: RGBA = { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
|
||||
export const BLUE: RGBA = { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
|
||||
|
||||
export const YELLOW: RGBA = { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };
|
||||
export const CYAN: RGBA = { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
|
||||
export const MAGENTA: RGBA = { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
|
||||
|
||||
export const WHITE: RGBA = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
|
||||
export const BLACK: RGBA = { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
}
|
||||
import { TBrand } from './util/type_util';
|
||||
|
||||
export type RGBA = {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number
|
||||
}
|
||||
|
||||
export type RGBA_255 = TBrand<RGBA, '255'>;
|
||||
|
||||
export namespace RGBAUtil {
|
||||
|
||||
export function toString(a: RGBA) {
|
||||
return `(${a.r}, ${a.g}, ${a.b}, ${a.a})`;
|
||||
}
|
||||
|
||||
// h: [0, 360], s: [0, 1], v: [0, 1]
|
||||
export function fromHSV(h: number, s: number, v: number): RGBA {
|
||||
const c = v * s;
|
||||
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
const m = v - c;
|
||||
|
||||
let output = OtS_Colours.BLACK;
|
||||
if (h < 60) {
|
||||
output.r = c;
|
||||
output.g = x;
|
||||
output.b = 0;
|
||||
} else if (h < 120) {
|
||||
output.r = x;
|
||||
output.g = c;
|
||||
output.b = 0;
|
||||
} else if (h < 180) {
|
||||
output.r = 0;
|
||||
output.g = c;
|
||||
output.b = x;
|
||||
} else if (h < 240) {
|
||||
output.r = 0;
|
||||
output.g = x;
|
||||
output.b = c;
|
||||
} else if (h < 300) {
|
||||
output.r = x;
|
||||
output.g = 0;
|
||||
output.b = c;
|
||||
} else {
|
||||
output.r = c;
|
||||
output.g = 0;
|
||||
output.b = x;
|
||||
}
|
||||
|
||||
output.r += m;
|
||||
output.g += m;
|
||||
output.b += m;
|
||||
output.a = 1.0;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function randomPretty(): RGBA {
|
||||
const hue = Math.random() * 360;
|
||||
const sat = 65/255;
|
||||
const val = 85/255;
|
||||
|
||||
return RGBAUtil.fromHSV(hue, sat, val);
|
||||
}
|
||||
|
||||
export function random(): RGBA {
|
||||
return {
|
||||
r: Math.random(),
|
||||
g: Math.random(),
|
||||
b: Math.random(),
|
||||
a: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
export function toHexString(a: RGBA) {
|
||||
const r = Math.floor(255 * a.r).toString(16).padStart(2, '0');
|
||||
const g = Math.floor(255 * a.g).toString(16).padStart(2, '0');
|
||||
const b = Math.floor(255 * a.b).toString(16).padStart(2, '0');
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
|
||||
export function fromHexString(str: string) {
|
||||
return {
|
||||
r: parseInt(str.substring(1, 3), 16) / 255,
|
||||
g: parseInt(str.substring(3, 5), 16) / 255,
|
||||
b: parseInt(str.substring(5, 7), 16) / 255,
|
||||
a: 1.0,
|
||||
};
|
||||
}
|
||||
|
||||
export function toUint8String(a: RGBA) {
|
||||
return `(${Math.floor(255 * a.r)}, ${Math.floor(255 * a.g)}, ${Math.floor(255 * a.b)}, ${Math.floor(255 * a.a)})`;
|
||||
}
|
||||
|
||||
export function toRGBA255(c: RGBA): RGBA_255 {
|
||||
const out: RGBA = {
|
||||
r: c.r * 255,
|
||||
g: c.r * 255,
|
||||
b: c.r * 255,
|
||||
a: c.r * 255,
|
||||
};
|
||||
return out as RGBA_255;
|
||||
}
|
||||
|
||||
export function fromRGBA255(c: RGBA_255): RGBA {
|
||||
const out: RGBA = {
|
||||
r: c.r / 255,
|
||||
g: c.g / 255,
|
||||
b: c.b / 255,
|
||||
a: c.a / 255,
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
export function add(a: RGBA, b: RGBA) {
|
||||
a.r += b.r;
|
||||
a.g += b.g;
|
||||
a.b += b.b;
|
||||
a.a += b.a;
|
||||
}
|
||||
|
||||
export function lerp(a: RGBA, b: RGBA, alpha: number) {
|
||||
const invAlpha = 1 - alpha;
|
||||
return {
|
||||
r: a.r * invAlpha + b.r * alpha,
|
||||
g: a.g * invAlpha + b.g * alpha,
|
||||
b: a.b * invAlpha + b.b * alpha,
|
||||
a: a.a * invAlpha + b.a * alpha,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Note this is a very naive approach to averaging a colour
|
||||
*/
|
||||
export function average(...colours: RGBA[]) {
|
||||
const avg = { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
for (let i = 0; i < colours.length; ++i) {
|
||||
avg.r += colours[i].r;
|
||||
avg.g += colours[i].g;
|
||||
avg.b += colours[i].b;
|
||||
avg.a += colours[i].a;
|
||||
}
|
||||
avg.r /= colours.length;
|
||||
avg.g /= colours.length;
|
||||
avg.b /= colours.length;
|
||||
avg.a /= colours.length;
|
||||
return avg;
|
||||
}
|
||||
|
||||
export function squaredDistance(a: RGBA, b: RGBA) {
|
||||
let squaredDistance = 0.0;
|
||||
squaredDistance += (a.r - b.r) * (a.r - b.r);
|
||||
squaredDistance += (a.g - b.g) * (a.g - b.g);
|
||||
squaredDistance += (a.b - b.b) * (a.b - b.b);
|
||||
squaredDistance += (a.a - b.a) * (a.a - b.a); // * AppConfig.Get.ALPHA_BIAS; TODO: ConfigRework
|
||||
return squaredDistance;
|
||||
}
|
||||
|
||||
export function copy(a: RGBA): RGBA {
|
||||
return {
|
||||
r: a.r,
|
||||
g: a.g,
|
||||
b: a.b,
|
||||
a: a.a,
|
||||
};
|
||||
}
|
||||
|
||||
export function copy255(a: RGBA_255): RGBA_255 {
|
||||
return {
|
||||
r: a.r,
|
||||
g: a.g,
|
||||
b: a.b,
|
||||
a: a.a,
|
||||
} as RGBA_255;
|
||||
}
|
||||
|
||||
export function toArray(a: RGBA): number[] {
|
||||
return [a.r, a.g, a.b, a.a];
|
||||
}
|
||||
|
||||
export function bin(col: RGBA, resolution: TColourAccuracy) {
|
||||
const binnedColour: RGBA = {
|
||||
r: Math.floor(Math.floor(col.r * resolution) * (255 / resolution)),
|
||||
g: Math.floor(Math.floor(col.g * resolution) * (255 / resolution)),
|
||||
b: Math.floor(Math.floor(col.b * resolution) * (255 / resolution)),
|
||||
a: Math.floor(Math.ceil(col.a * resolution) * (255 / resolution)),
|
||||
};
|
||||
|
||||
return binnedColour as RGBA_255;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a colour as a single number.
|
||||
* Note this will bin colours together.
|
||||
* @param col The colour to hash.
|
||||
* @param resolution An uint8, the larger the more accurate the hash.
|
||||
*/
|
||||
export function hash(col: RGBA, resolution: TColourAccuracy): number {
|
||||
const r = Math.floor(col.r * resolution);
|
||||
const g = Math.floor(col.g * resolution);
|
||||
const b = Math.floor(col.b * resolution);
|
||||
const a = Math.floor(col.a * resolution);
|
||||
|
||||
let hash = r;
|
||||
hash = (hash << 8) + g;
|
||||
hash = (hash << 8) + b;
|
||||
hash = (hash << 8) + a;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function hash255(col: RGBA_255) {
|
||||
let hash = col.r;
|
||||
hash = (hash << 8) + col.g;
|
||||
hash = (hash << 8) + col.b;
|
||||
hash = (hash << 8) + col.a;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export type TColourAccuracy = number;
|
||||
}
|
||||
|
||||
export class OtS_Colours {
|
||||
public static get BLACK(): RGBA {
|
||||
return { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get WHITE(): RGBA {
|
||||
return { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get RED(): RGBA {
|
||||
return { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get GREEN(): RGBA {
|
||||
return { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get BLUE(): RGBA {
|
||||
return { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get YELLOW(): RGBA {
|
||||
return { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get CYAN(): RGBA {
|
||||
return { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
|
||||
}
|
||||
|
||||
public static get MAGENTA(): RGBA {
|
||||
return { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
|
||||
}
|
||||
}
|
@ -1,126 +1,122 @@
|
||||
|
||||
import { AppTypes } from './util';
|
||||
import { AppPaths, PathUtil } from './util/path_util';
|
||||
|
||||
export namespace AppConstants {
|
||||
export const FACES_PER_VOXEL = 6;
|
||||
export const VERTICES_PER_FACE = 4;
|
||||
export const INDICES_PER_VOXEL = 24;
|
||||
export const COMPONENT_PER_SIZE_OFFSET = FACES_PER_VOXEL * VERTICES_PER_FACE;
|
||||
|
||||
export namespace ComponentSize {
|
||||
export const LIGHTING = 1;
|
||||
export const TEXCOORD = 2;
|
||||
export const POSITION = 3;
|
||||
export const COLOUR = 4;
|
||||
export const NORMAL = 3;
|
||||
export const INDICES = 3;
|
||||
export const OCCLUSION = 4;
|
||||
}
|
||||
|
||||
export namespace VoxelMeshBufferComponentOffsets {
|
||||
export const LIGHTING = ComponentSize.LIGHTING * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const TEXCOORD = ComponentSize.TEXCOORD * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const POSITION = ComponentSize.POSITION * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const COLOUR = ComponentSize.COLOUR * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const NORMAL = ComponentSize.NORMAL * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const INDICES = 36;
|
||||
export const OCCLUSION = ComponentSize.OCCLUSION * COMPONENT_PER_SIZE_OFFSET;
|
||||
}
|
||||
|
||||
export const DATA_VERSION = 3105; // 1.19
|
||||
}
|
||||
|
||||
export class AppRuntimeConstants {
|
||||
/* Singleton */
|
||||
private static _instance: AppRuntimeConstants;
|
||||
public static get Get() {
|
||||
return this._instance || (this._instance = new this());
|
||||
}
|
||||
|
||||
public readonly FALLABLE_BLOCKS = new Set([
|
||||
'minecraft:anvil',
|
||||
'minecraft:lime_concrete_powder',
|
||||
'minecraft:orange_concrete_powder',
|
||||
'minecraft:black_concrete_powder',
|
||||
'minecraft:brown_concrete_powder',
|
||||
'minecraft:cyan_concrete_powder',
|
||||
'minecraft:light_gray_concrete_powder',
|
||||
'minecraft:purple_concrete_powder',
|
||||
'minecraft:magenta_concrete_powder',
|
||||
'minecraft:light_blue_concrete_powder',
|
||||
'minecraft:yellow_concrete_powder',
|
||||
'minecraft:white_concrete_powder',
|
||||
'minecraft:blue_concrete_powder',
|
||||
'minecraft:red_concrete_powder',
|
||||
'minecraft:gray_concrete_powder',
|
||||
'minecraft:pink_concrete_powder',
|
||||
'minecraft:green_concrete_powder',
|
||||
'minecraft:dragon_egg',
|
||||
'minecraft:gravel',
|
||||
'minecraft:pointed_dripstone',
|
||||
'minecraft:red_sand',
|
||||
'minecraft:sand',
|
||||
'minecraft:scaffolding',
|
||||
]);
|
||||
|
||||
public readonly TRANSPARENT_BLOCKS = new Set([
|
||||
'minecraft:frosted_ice',
|
||||
'minecraft:glass',
|
||||
'minecraft:white_stained_glass',
|
||||
'minecraft:orange_stained_glass',
|
||||
'minecraft:magenta_stained_glass',
|
||||
'minecraft:light_blue_stained_glass',
|
||||
'minecraft:yellow_stained_glass',
|
||||
'minecraft:lime_stained_glass',
|
||||
'minecraft:pink_stained_glass',
|
||||
'minecraft:gray_stained_glass',
|
||||
'minecraft:light_gray_stained_glass',
|
||||
'minecraft:cyan_stained_glass',
|
||||
'minecraft:purple_stained_glass',
|
||||
'minecraft:blue_stained_glass',
|
||||
'minecraft:brown_stained_glass',
|
||||
'minecraft:green_stained_glass',
|
||||
'minecraft:red_stained_glass',
|
||||
'minecraft:black_stained_glass',
|
||||
'minecraft:ice',
|
||||
'minecraft:oak_leaves',
|
||||
'minecraft:spruce_leaves',
|
||||
'minecraft:birch_leaves',
|
||||
'minecraft:jungle_leaves',
|
||||
'minecraft:acacia_leaves',
|
||||
'minecraft:dark_oak_leaves',
|
||||
'minecraft:mangrove_leaves',
|
||||
'minecraft:azalea_leaves',
|
||||
'minecraft:flowering_azalea_leaves',
|
||||
'minecraft:slime_block',
|
||||
'minecraft:honey_block',
|
||||
]);
|
||||
|
||||
public readonly GRASS_LIKE_BLOCKS = new Set([
|
||||
'minecraft:grass_block',
|
||||
'minecraft:grass_path',
|
||||
'minecraft:podzol',
|
||||
'minecraft:crimson_nylium',
|
||||
'minecraft:warped_nylium',
|
||||
'minecraft:mycelium',
|
||||
'minecraft:farmland',
|
||||
]);
|
||||
|
||||
public readonly EMISSIVE_BLOCKS = new Set([
|
||||
'minecraft:respawn_anchor',
|
||||
'minecraft:magma_block',
|
||||
'minecraft:sculk_catalyst',
|
||||
'minecraft:crying_obsidian',
|
||||
'minecraft:shroomlight',
|
||||
'minecraft:sea_lantern',
|
||||
'minecraft:jack_o_lantern',
|
||||
'minecraft:glowstone',
|
||||
'minecraft:pearlescent_froglight',
|
||||
'minecraft:verdant_froglight',
|
||||
'minecraft:ochre_froglight',
|
||||
]);
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
}
|
||||
export namespace AppConstants {
|
||||
export const FACES_PER_VOXEL = 6;
|
||||
export const VERTICES_PER_FACE = 4;
|
||||
export const INDICES_PER_VOXEL = 24;
|
||||
export const COMPONENT_PER_SIZE_OFFSET = FACES_PER_VOXEL * VERTICES_PER_FACE;
|
||||
|
||||
export namespace ComponentSize {
|
||||
export const LIGHTING = 1;
|
||||
export const TEXCOORD = 2;
|
||||
export const POSITION = 3;
|
||||
export const COLOUR = 4;
|
||||
export const NORMAL = 3;
|
||||
export const INDICES = 3;
|
||||
export const OCCLUSION = 4;
|
||||
}
|
||||
|
||||
export namespace VoxelMeshBufferComponentOffsets {
|
||||
export const LIGHTING = ComponentSize.LIGHTING * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const TEXCOORD = ComponentSize.TEXCOORD * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const POSITION = ComponentSize.POSITION * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const COLOUR = ComponentSize.COLOUR * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const NORMAL = ComponentSize.NORMAL * COMPONENT_PER_SIZE_OFFSET;
|
||||
export const INDICES = 36;
|
||||
export const OCCLUSION = ComponentSize.OCCLUSION * COMPONENT_PER_SIZE_OFFSET;
|
||||
}
|
||||
|
||||
export const DATA_VERSION = 3105; // 1.19
|
||||
}
|
||||
|
||||
export class AppRuntimeConstants {
|
||||
/* Singleton */
|
||||
private static _instance: AppRuntimeConstants;
|
||||
public static get Get() {
|
||||
return this._instance || (this._instance = new this());
|
||||
}
|
||||
|
||||
public readonly FALLABLE_BLOCKS = new Set([
|
||||
'minecraft:anvil',
|
||||
'minecraft:lime_concrete_powder',
|
||||
'minecraft:orange_concrete_powder',
|
||||
'minecraft:black_concrete_powder',
|
||||
'minecraft:brown_concrete_powder',
|
||||
'minecraft:cyan_concrete_powder',
|
||||
'minecraft:light_gray_concrete_powder',
|
||||
'minecraft:purple_concrete_powder',
|
||||
'minecraft:magenta_concrete_powder',
|
||||
'minecraft:light_blue_concrete_powder',
|
||||
'minecraft:yellow_concrete_powder',
|
||||
'minecraft:white_concrete_powder',
|
||||
'minecraft:blue_concrete_powder',
|
||||
'minecraft:red_concrete_powder',
|
||||
'minecraft:gray_concrete_powder',
|
||||
'minecraft:pink_concrete_powder',
|
||||
'minecraft:green_concrete_powder',
|
||||
'minecraft:dragon_egg',
|
||||
'minecraft:gravel',
|
||||
'minecraft:pointed_dripstone',
|
||||
'minecraft:red_sand',
|
||||
'minecraft:sand',
|
||||
'minecraft:scaffolding',
|
||||
]);
|
||||
|
||||
public readonly TRANSPARENT_BLOCKS = new Set([
|
||||
'minecraft:frosted_ice',
|
||||
'minecraft:glass',
|
||||
'minecraft:white_stained_glass',
|
||||
'minecraft:orange_stained_glass',
|
||||
'minecraft:magenta_stained_glass',
|
||||
'minecraft:light_blue_stained_glass',
|
||||
'minecraft:yellow_stained_glass',
|
||||
'minecraft:lime_stained_glass',
|
||||
'minecraft:pink_stained_glass',
|
||||
'minecraft:gray_stained_glass',
|
||||
'minecraft:light_gray_stained_glass',
|
||||
'minecraft:cyan_stained_glass',
|
||||
'minecraft:purple_stained_glass',
|
||||
'minecraft:blue_stained_glass',
|
||||
'minecraft:brown_stained_glass',
|
||||
'minecraft:green_stained_glass',
|
||||
'minecraft:red_stained_glass',
|
||||
'minecraft:black_stained_glass',
|
||||
'minecraft:ice',
|
||||
'minecraft:oak_leaves',
|
||||
'minecraft:spruce_leaves',
|
||||
'minecraft:birch_leaves',
|
||||
'minecraft:jungle_leaves',
|
||||
'minecraft:acacia_leaves',
|
||||
'minecraft:dark_oak_leaves',
|
||||
'minecraft:mangrove_leaves',
|
||||
'minecraft:azalea_leaves',
|
||||
'minecraft:flowering_azalea_leaves',
|
||||
'minecraft:slime_block',
|
||||
'minecraft:honey_block',
|
||||
]);
|
||||
|
||||
public readonly GRASS_LIKE_BLOCKS = new Set([
|
||||
'minecraft:grass_block',
|
||||
'minecraft:grass_path',
|
||||
'minecraft:podzol',
|
||||
'minecraft:crimson_nylium',
|
||||
'minecraft:warped_nylium',
|
||||
'minecraft:mycelium',
|
||||
'minecraft:farmland',
|
||||
]);
|
||||
|
||||
public readonly EMISSIVE_BLOCKS = new Set([
|
||||
'minecraft:respawn_anchor',
|
||||
'minecraft:magma_block',
|
||||
'minecraft:sculk_catalyst',
|
||||
'minecraft:crying_obsidian',
|
||||
'minecraft:shroomlight',
|
||||
'minecraft:sea_lantern',
|
||||
'minecraft:jack_o_lantern',
|
||||
'minecraft:glowstone',
|
||||
'minecraft:pearlescent_froglight',
|
||||
'minecraft:verdant_froglight',
|
||||
'minecraft:ochre_froglight',
|
||||
]);
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
}
|
@ -1,47 +1,46 @@
|
||||
import { RGBA_255 } from './colour';
|
||||
import { AppConfig } from './config';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
|
||||
export class Ditherer {
|
||||
public static ditherRandom(colour: RGBA_255, magnitude: number) {
|
||||
const offset = (Math.random() - 0.5) * magnitude;
|
||||
|
||||
colour.r += offset;
|
||||
colour.g += offset;
|
||||
colour.b += offset;
|
||||
}
|
||||
|
||||
public static ditherOrdered(colour: RGBA_255, position: Vector3, magnitude: number) {
|
||||
const map = this._getThresholdValue(
|
||||
Math.abs(position.x % 4),
|
||||
Math.abs(position.y % 4),
|
||||
Math.abs(position.z % 4),
|
||||
);
|
||||
|
||||
const offset = map * magnitude;
|
||||
|
||||
colour.r += offset;
|
||||
colour.g += offset;
|
||||
colour.b += offset;
|
||||
}
|
||||
|
||||
private static _mapMatrix = [
|
||||
0, 16, 2, 18, 48, 32, 50, 34,
|
||||
6, 22, 4, 20, 54, 38, 52, 36,
|
||||
24, 40, 26, 42, 8, 56, 10, 58,
|
||||
30, 46, 28, 44, 14, 62, 12, 60,
|
||||
3, 19, 5, 21, 51, 35, 53, 37,
|
||||
1, 17, 7, 23, 49, 33, 55, 39,
|
||||
27, 43, 29, 45, 11, 59, 13, 61,
|
||||
25, 41, 31, 47, 9, 57, 15, 63,
|
||||
];
|
||||
|
||||
private static _getThresholdValue(x: number, y: number, z: number) {
|
||||
const size = 4;
|
||||
ASSERT(0 <= x && x < size && 0 <= y && y < size && 0 <= z && z < size);
|
||||
const index = (x + (size * y) + (size * size * z));
|
||||
ASSERT(0 <= index && index < size * size * size);
|
||||
return (Ditherer._mapMatrix[index] / (size * size * size)) - 0.5;
|
||||
}
|
||||
}
|
||||
import { RGBA_255 } from './colour';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
|
||||
export class Ditherer {
|
||||
public static ditherRandom(colour: RGBA_255, magnitude: number) {
|
||||
const offset = (Math.random() - 0.5) * magnitude;
|
||||
|
||||
colour.r += offset;
|
||||
colour.g += offset;
|
||||
colour.b += offset;
|
||||
}
|
||||
|
||||
public static ditherOrdered(colour: RGBA_255, position: Vector3, magnitude: number) {
|
||||
const map = this._getThresholdValue(
|
||||
Math.abs(position.x % 4),
|
||||
Math.abs(position.y % 4),
|
||||
Math.abs(position.z % 4),
|
||||
);
|
||||
|
||||
const offset = map * magnitude;
|
||||
|
||||
colour.r += offset;
|
||||
colour.g += offset;
|
||||
colour.b += offset;
|
||||
}
|
||||
|
||||
private static _mapMatrix = [
|
||||
0, 16, 2, 18, 48, 32, 50, 34,
|
||||
6, 22, 4, 20, 54, 38, 52, 36,
|
||||
24, 40, 26, 42, 8, 56, 10, 58,
|
||||
30, 46, 28, 44, 14, 62, 12, 60,
|
||||
3, 19, 5, 21, 51, 35, 53, 37,
|
||||
1, 17, 7, 23, 49, 33, 55, 39,
|
||||
27, 43, 29, 45, 11, 59, 13, 61,
|
||||
25, 41, 31, 47, 9, 57, 15, 63,
|
||||
];
|
||||
|
||||
private static _getThresholdValue(x: number, y: number, z: number) {
|
||||
const size = 4;
|
||||
ASSERT(0 <= x && x < size && 0 <= y && y < size && 0 <= z && z < size);
|
||||
const index = (x + (size * y) + (size * size * z));
|
||||
ASSERT(0 <= index && index < size * size * size);
|
||||
return (Ditherer._mapMatrix[index] / (size * size * size)) - 0.5;
|
||||
}
|
||||
}
|
@ -1,24 +1,25 @@
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
|
||||
export type TStructureRegion = { name: string, content: Buffer };
|
||||
|
||||
export type TStructureExport =
|
||||
| { type: 'single', extension: string, content: Buffer }
|
||||
| { type: 'multiple', extension: string, regions: TStructureRegion[] }
|
||||
|
||||
export abstract class IExporter {
|
||||
/** The file type extension of this exporter.
|
||||
* @note Do not include the dot prefix, e.g. 'obj' not '.obj'.
|
||||
*/
|
||||
public abstract getFormatFilter(): {
|
||||
name: string,
|
||||
extension: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a block mesh to a file.
|
||||
* @param blockMesh The block mesh to export.
|
||||
* @param filePath The location to save the file to.
|
||||
*/
|
||||
public abstract export(blockMesh: BlockMesh): TStructureExport;
|
||||
}
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
|
||||
export type TStructureRegion = { name: string, content: Uint8Array };
|
||||
|
||||
export type TStructureExport =
|
||||
| { type: 'single', extension: string, content: Uint8Array }
|
||||
| { type: 'multiple', extension: string, regions: TStructureRegion[] }
|
||||
|
||||
export abstract class IExporter {
|
||||
/** The file type extension of this exporter.
|
||||
* @note Do not include the dot prefix, e.g. 'obj' not '.obj'.
|
||||
*/
|
||||
public abstract getFormatFilter(): {
|
||||
name: string,
|
||||
extension: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a block mesh to a file.
|
||||
* @param blockMesh The block mesh to export.
|
||||
* @param filePath The location to save the file to.
|
||||
*/
|
||||
public abstract export(blockMesh: OtS_BlockMesh): TStructureExport;
|
||||
}
|
@ -1,34 +1,34 @@
|
||||
import { IExporter } from './base_exporter';
|
||||
import { IndexedJSONExporter } from './indexed_json_exporter ';
|
||||
import { Litematic } from './litematic_exporter';
|
||||
import { NBTExporter } from './nbt_exporter';
|
||||
import { SchemExporter } from './schem_exporter';
|
||||
import { Schematic } from './schematic_exporter';
|
||||
import { UncompressedJSONExporter } from './uncompressed_json_exporter';
|
||||
|
||||
export type TExporters =
|
||||
'schematic' |
|
||||
'litematic' |
|
||||
'schem' |
|
||||
'nbt' |
|
||||
'uncompressed_json' |
|
||||
'indexed_json';
|
||||
|
||||
export class ExporterFactory {
|
||||
public static GetExporter(voxeliser: TExporters): IExporter {
|
||||
switch (voxeliser) {
|
||||
case 'schematic':
|
||||
return new Schematic();
|
||||
case 'litematic':
|
||||
return new Litematic();
|
||||
case 'schem':
|
||||
return new SchemExporter();
|
||||
case 'nbt':
|
||||
return new NBTExporter();
|
||||
case 'uncompressed_json':
|
||||
return new UncompressedJSONExporter();
|
||||
case 'indexed_json':
|
||||
return new IndexedJSONExporter();
|
||||
}
|
||||
}
|
||||
}
|
||||
import { IExporter } from './base_exporter';
|
||||
import { IndexedJSONExporter } from './indexed_json_exporter ';
|
||||
import { Litematic } from './litematic_exporter';
|
||||
import { NBTExporter } from './nbt_exporter';
|
||||
import { SchemExporter } from './schem_exporter';
|
||||
import { Schematic } from './schematic_exporter';
|
||||
import { UncompressedJSONExporter } from './uncompressed_json_exporter';
|
||||
|
||||
export type TExporters =
|
||||
'schematic' |
|
||||
'litematic' |
|
||||
'schem' |
|
||||
'nbt' |
|
||||
'uncompressed_json' |
|
||||
'indexed_json';
|
||||
|
||||
export class ExporterFactory {
|
||||
public static GetExporter(voxeliser: TExporters): IExporter {
|
||||
switch (voxeliser) {
|
||||
case 'schematic':
|
||||
return new Schematic();
|
||||
case 'litematic':
|
||||
return new Litematic();
|
||||
case 'schem':
|
||||
return new SchemExporter();
|
||||
case 'nbt':
|
||||
return new NBTExporter();
|
||||
case 'uncompressed_json':
|
||||
return new UncompressedJSONExporter();
|
||||
case 'indexed_json':
|
||||
return new IndexedJSONExporter();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
|
||||
export class IndexedJSONExporter extends IExporter {
|
||||
@ -9,10 +10,10 @@ export class IndexedJSONExporter extends IExporter {
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: BlockMesh): TStructureExport {
|
||||
public override export(blockMesh: OtS_BlockMesh): TStructureExport {
|
||||
const blocks = blockMesh.getBlocks();
|
||||
|
||||
const blocksUsed = blockMesh.getBlockPalette();
|
||||
const blocksUsed = Array.from(blockMesh.calcBlocksUsed());
|
||||
const blockToIndex = new Map<string, number>();
|
||||
const indexToBlock = new Map<number, string>();
|
||||
for (let i = 0; i < blocksUsed.length; ++i) {
|
||||
@ -23,10 +24,8 @@ export class IndexedJSONExporter extends IExporter {
|
||||
const blockArray = new Array<Array<number>>();
|
||||
|
||||
// Serialise all block except for the last one.
|
||||
for (let i = 0; i < blocks.length; ++i) {
|
||||
const block = blocks[i];
|
||||
const pos = block.voxel.position;
|
||||
blockArray.push([pos.x, pos.y, pos.z, blockToIndex.get(block.blockInfo.name)!]);
|
||||
for (const { position, name } of blockMesh.getBlocks()) {
|
||||
blockArray.push([position.x, position.y, position.z, blockToIndex.get(name)!]);
|
||||
}
|
||||
|
||||
const json = JSON.stringify({
|
||||
@ -34,6 +33,9 @@ export class IndexedJSONExporter extends IExporter {
|
||||
xyzi: blockArray,
|
||||
});
|
||||
|
||||
return { type: 'single', extension: '.json', content: Buffer.from(json) };
|
||||
const encoder = new TextEncoder();
|
||||
const buffer = encoder.encode(json);
|
||||
|
||||
return { type: 'single', extension: '.json', content: buffer };
|
||||
}
|
||||
}
|
@ -1,229 +1,233 @@
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { AppConstants } from '../constants';
|
||||
import { ceilToNearest } from '../math';
|
||||
import { AppTypes } from '../util';
|
||||
import { ASSERT } from '../util/error_util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
import { save } from '@loaders.gl/core';
|
||||
|
||||
type BlockID = number;
|
||||
type long = [number, number];
|
||||
type BlockMapping = Map<AppTypes.TNamespacedBlockName, BlockID>;
|
||||
|
||||
export class Litematic extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Litematic',
|
||||
extension: 'litematic',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: BlockMesh): TStructureExport {
|
||||
const nbt = this._convertToNBT(blockMesh);
|
||||
return { type: 'single', extension: '.litematic', content: saveNBT(nbt) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mapping from block names to their respecitve index in the block state palette.
|
||||
*/
|
||||
private _createBlockMapping(blockMesh: BlockMesh): BlockMapping {
|
||||
const blockMapping: BlockMapping = new Map();
|
||||
blockMapping.set('minecraft:air', 0);
|
||||
|
||||
blockMesh.getBlockPalette().forEach((blockName, index) => {
|
||||
blockMapping.set(blockName, index + 1);
|
||||
});
|
||||
|
||||
return blockMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the blocks into a buffer that's the dimensions of the block mesh.
|
||||
*/
|
||||
private _createBlockBuffer(blockMesh: BlockMesh, blockMapping: BlockMapping): Uint32Array {
|
||||
const bounds = blockMesh.getVoxelMesh()?.getBounds();
|
||||
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
|
||||
|
||||
const buffer = new Uint32Array(sizeVector.x * sizeVector.y * sizeVector.z);
|
||||
|
||||
blockMesh.getBlocks().forEach((block) => {
|
||||
const indexVector = Vector3.sub(block.voxel.position, bounds.min);
|
||||
const bufferIndex = (sizeVector.z * sizeVector.x * indexVector.y) + (sizeVector.x * indexVector.z) + indexVector.x; // XZY ordering
|
||||
|
||||
const mappingIndex = blockMapping.get(block.blockInfo.name);
|
||||
ASSERT(mappingIndex !== undefined, 'Invalid mapping index');
|
||||
|
||||
buffer[bufferIndex] = mappingIndex;
|
||||
});
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private _createBlockStates(blockMesh: BlockMesh, blockMapping: BlockMapping) {
|
||||
const buffer = this._encodeBlockBuffer(blockMesh, blockMapping);
|
||||
|
||||
const numBytes = buffer.length;
|
||||
const numBits = numBytes * 8;
|
||||
|
||||
const blockStates = new Array<long>(Math.ceil(numBits / 64));
|
||||
|
||||
let index = 0;
|
||||
for (let i = numBits; i > 0; i -= 64) {
|
||||
const rightBaseIndexBit = i - 32;
|
||||
const rightBaseIndexByte = rightBaseIndexBit / 8;
|
||||
|
||||
let right = 0;
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 0];
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 1];
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 2];
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 3];
|
||||
|
||||
const leftBaseIndexBit = i - 64;
|
||||
const leftBaseIndexByte = leftBaseIndexBit / 8;
|
||||
|
||||
let left = 0;
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 0];
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 1];
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 2];
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 3];
|
||||
|
||||
blockStates[index++] = [left, right];
|
||||
}
|
||||
|
||||
return blockStates;
|
||||
}
|
||||
|
||||
private _encodeBlockBuffer(blockMesh: BlockMesh, blockMapping: BlockMapping) {
|
||||
const blockBuffer = this._createBlockBuffer(blockMesh, blockMapping);
|
||||
|
||||
const paletteSize = blockMapping.size;
|
||||
const stride = Math.ceil(Math.log2(paletteSize - 1));
|
||||
ASSERT(stride >= 1, `Stride too small: ${stride}`);
|
||||
|
||||
const expectedLengthBits = blockBuffer.length * stride;
|
||||
const requiredLengthBits = ceilToNearest(expectedLengthBits, 64);
|
||||
const startOffsetBits = requiredLengthBits - expectedLengthBits;
|
||||
|
||||
const requiredLengthBytes = requiredLengthBits / 8;
|
||||
const buffer = Buffer.alloc(requiredLengthBytes);
|
||||
|
||||
// Write first few offset bits
|
||||
const fullBytesToWrite = Math.floor(startOffsetBits / 8);
|
||||
for (let i = 0; i < fullBytesToWrite; ++i) {
|
||||
buffer[i] = 0;
|
||||
}
|
||||
|
||||
const remainingBitsToWrite = startOffsetBits - (fullBytesToWrite * 8);
|
||||
let currentByte = 0;
|
||||
let bitsWrittenToByte = remainingBitsToWrite;
|
||||
let nextBufferWriteIndex = fullBytesToWrite;
|
||||
|
||||
for (let i = blockBuffer.length - 1; i >= 0; --i) {
|
||||
for (let j = 0; j < stride; ++j) {
|
||||
if (bitsWrittenToByte === 8) {
|
||||
buffer[nextBufferWriteIndex] = currentByte;
|
||||
++nextBufferWriteIndex;
|
||||
currentByte = 0; // Shouldn't be actually necessary to reset
|
||||
bitsWrittenToByte = 0;
|
||||
}
|
||||
|
||||
const bitToAddToByte = (blockBuffer[i] >> (stride - j - 1)) & 1;
|
||||
currentByte = (currentByte << 1) + bitToAddToByte;
|
||||
++bitsWrittenToByte;
|
||||
}
|
||||
}
|
||||
|
||||
// Write remaining partially filled byte
|
||||
buffer[nextBufferWriteIndex] = currentByte;
|
||||
++nextBufferWriteIndex;
|
||||
currentByte = 0; // Shouldn't be actually necessary to reset
|
||||
bitsWrittenToByte = 0;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private _createBlockStatePalette(blockMapping: BlockMapping) {
|
||||
const blockStatePalette = Array(Object.keys(blockMapping).length);
|
||||
|
||||
blockMapping.forEach((index, blockName) => {
|
||||
blockStatePalette[index] = { Name: { type: TagType.String, value: blockName } };
|
||||
});
|
||||
blockStatePalette[0] = { Name: { type: TagType.String, value: 'minecraft:air' } };
|
||||
|
||||
return blockStatePalette;
|
||||
}
|
||||
|
||||
private _convertToNBT(blockMesh: BlockMesh) {
|
||||
const bounds = blockMesh.getVoxelMesh()?.getBounds();
|
||||
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
|
||||
|
||||
const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z;
|
||||
const blockMapping = this._createBlockMapping(blockMesh);
|
||||
|
||||
const blockStates = this._createBlockStates(blockMesh, blockMapping);
|
||||
const blockStatePalette = this._createBlockStatePalette(blockMapping);
|
||||
const numBlocks = blockMesh.getBlocks().length;
|
||||
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'Litematic',
|
||||
value: {
|
||||
Metadata: {
|
||||
type: TagType.Compound, value: {
|
||||
Author: { type: TagType.String, value: '' },
|
||||
Description: { type: TagType.String, value: '' },
|
||||
Size: {
|
||||
type: TagType.Compound, value: {
|
||||
x: { type: TagType.Int, value: sizeVector.x },
|
||||
y: { type: TagType.Int, value: sizeVector.y },
|
||||
z: { type: TagType.Int, value: sizeVector.z },
|
||||
},
|
||||
},
|
||||
Name: { type: TagType.String, value: '' },
|
||||
RegionCount: { type: TagType.Int, value: 1 },
|
||||
TimeCreated: { type: TagType.Long, value: [0, 0] },
|
||||
TimeModified: { type: TagType.Long, value: [0, 0] },
|
||||
TotalBlocks: { type: TagType.Int, value: numBlocks },
|
||||
TotalVolume: { type: TagType.Int, value: bufferSize },
|
||||
},
|
||||
},
|
||||
Regions: {
|
||||
type: TagType.Compound, value: {
|
||||
Unnamed: {
|
||||
type: TagType.Compound, value: {
|
||||
BlockStates: { type: TagType.LongArray, value: blockStates },
|
||||
PendingBlockTicks: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
Position: {
|
||||
type: TagType.Compound, value: {
|
||||
x: { type: TagType.Int, value: 0 },
|
||||
y: { type: TagType.Int, value: 0 },
|
||||
z: { type: TagType.Int, value: 0 },
|
||||
},
|
||||
},
|
||||
BlockStatePalette: { type: TagType.List, value: { type: TagType.Compound, value: blockStatePalette } },
|
||||
Size: {
|
||||
type: TagType.Compound, value: {
|
||||
x: { type: TagType.Int, value: sizeVector.x },
|
||||
y: { type: TagType.Int, value: sizeVector.y },
|
||||
z: { type: TagType.Int, value: sizeVector.z },
|
||||
},
|
||||
},
|
||||
PendingFluidTicks: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
TileEntities: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
Entities: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MinecraftDataVersion: { type: TagType.Int, value: AppConstants.DATA_VERSION },
|
||||
Version: { type: TagType.Int, value: 5 },
|
||||
},
|
||||
};
|
||||
|
||||
return nbt;
|
||||
}
|
||||
}
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { AppConstants } from '../constants';
|
||||
import { ceilToNearest } from '../math';
|
||||
import { AppTypes } from '../util';
|
||||
import { ASSERT } from '../util/error_util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
|
||||
type BlockID = number;
|
||||
type long = [number, number];
|
||||
type BlockMapping = Map<AppTypes.TNamespacedBlockName, BlockID>;
|
||||
|
||||
export class Litematic extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Litematic',
|
||||
extension: 'litematic',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: OtS_BlockMesh): TStructureExport {
|
||||
const nbt = this._convertToNBT(blockMesh);
|
||||
return { type: 'single', extension: '.litematic', content: saveNBT(nbt) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mapping from block names to their respecitve index in the block state palette.
|
||||
*/
|
||||
private _createBlockMapping(blockMesh: OtS_BlockMesh): BlockMapping {
|
||||
const blockMapping: BlockMapping = new Map();
|
||||
blockMapping.set('minecraft:air', 0);
|
||||
|
||||
let index = 1;
|
||||
blockMesh.calcBlocksUsed().forEach((blockName) => {
|
||||
blockMapping.set(blockName, index);
|
||||
++index;
|
||||
});
|
||||
|
||||
return blockMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the blocks into a buffer that's the dimensions of the block mesh.
|
||||
*/
|
||||
private _createBlockBuffer(blockMesh: OtS_BlockMesh, blockMapping: BlockMapping): Uint32Array {
|
||||
const bounds = blockMesh.getBounds();
|
||||
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
|
||||
|
||||
const buffer = new Uint32Array(sizeVector.x * sizeVector.y * sizeVector.z);
|
||||
|
||||
for (const { position, name } of blockMesh.getBlocks()) {
|
||||
const indexVector = Vector3.sub(position, bounds.min);
|
||||
const bufferIndex = (sizeVector.z * sizeVector.x * indexVector.y) + (sizeVector.x * indexVector.z) + indexVector.x; // XZY ordering
|
||||
|
||||
const mappingIndex = blockMapping.get(name);
|
||||
ASSERT(mappingIndex !== undefined, 'Invalid mapping index');
|
||||
|
||||
buffer[bufferIndex] = mappingIndex;
|
||||
};
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private _createBlockStates(blockMesh: OtS_BlockMesh, blockMapping: BlockMapping) {
|
||||
const buffer = this._encodeBlockBuffer(blockMesh, blockMapping);
|
||||
|
||||
const numBytes = buffer.length;
|
||||
const numBits = numBytes * 8;
|
||||
|
||||
const blockStates = new Array<long>(Math.ceil(numBits / 64));
|
||||
|
||||
let index = 0;
|
||||
for (let i = numBits; i > 0; i -= 64) {
|
||||
const rightBaseIndexBit = i - 32;
|
||||
const rightBaseIndexByte = rightBaseIndexBit / 8;
|
||||
|
||||
let right = 0;
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 0];
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 1];
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 2];
|
||||
right = (right << 8) + buffer[rightBaseIndexByte + 3];
|
||||
|
||||
const leftBaseIndexBit = i - 64;
|
||||
const leftBaseIndexByte = leftBaseIndexBit / 8;
|
||||
|
||||
let left = 0;
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 0];
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 1];
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 2];
|
||||
left = (left << 8) + buffer[leftBaseIndexByte + 3];
|
||||
|
||||
blockStates[index++] = [left, right];
|
||||
}
|
||||
|
||||
return blockStates;
|
||||
}
|
||||
|
||||
private _encodeBlockBuffer(blockMesh: OtS_BlockMesh, blockMapping: BlockMapping) {
|
||||
const blockBuffer = this._createBlockBuffer(blockMesh, blockMapping);
|
||||
|
||||
const paletteSize = blockMapping.size;
|
||||
ASSERT(paletteSize >= 2, `Palette too small`);
|
||||
|
||||
let stride = Math.ceil(Math.log2(paletteSize));
|
||||
stride = Math.max(2, stride);
|
||||
|
||||
const expectedLengthBits = blockBuffer.length * stride;
|
||||
const requiredLengthBits = ceilToNearest(expectedLengthBits, 64);
|
||||
const startOffsetBits = requiredLengthBits - expectedLengthBits;
|
||||
|
||||
const requiredLengthBytes = requiredLengthBits / 8;
|
||||
const buffer = new Uint8Array(requiredLengthBytes);
|
||||
|
||||
// Write first few offset bits
|
||||
const fullBytesToWrite = Math.floor(startOffsetBits / 8);
|
||||
for (let i = 0; i < fullBytesToWrite; ++i) {
|
||||
buffer[i] = 0;
|
||||
}
|
||||
|
||||
const remainingBitsToWrite = startOffsetBits - (fullBytesToWrite * 8);
|
||||
let currentByte = 0;
|
||||
let bitsWrittenToByte = remainingBitsToWrite;
|
||||
let nextBufferWriteIndex = fullBytesToWrite;
|
||||
|
||||
for (let i = blockBuffer.length - 1; i >= 0; --i) {
|
||||
for (let j = 0; j < stride; ++j) {
|
||||
if (bitsWrittenToByte === 8) {
|
||||
buffer[nextBufferWriteIndex] = currentByte;
|
||||
++nextBufferWriteIndex;
|
||||
currentByte = 0; // Shouldn't be actually necessary to reset
|
||||
bitsWrittenToByte = 0;
|
||||
}
|
||||
|
||||
const bitToAddToByte = (blockBuffer[i] >> (stride - j - 1)) & 1;
|
||||
currentByte = (currentByte << 1) + bitToAddToByte;
|
||||
++bitsWrittenToByte;
|
||||
}
|
||||
}
|
||||
|
||||
// Write remaining partially filled byte
|
||||
buffer[nextBufferWriteIndex] = currentByte;
|
||||
++nextBufferWriteIndex;
|
||||
currentByte = 0; // Shouldn't be actually necessary to reset
|
||||
bitsWrittenToByte = 0;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private _createBlockStatePalette(blockMapping: BlockMapping) {
|
||||
const blockStatePalette = Array(Object.keys(blockMapping).length);
|
||||
|
||||
blockMapping.forEach((index, blockName) => {
|
||||
blockStatePalette[index] = { Name: { type: TagType.String, value: blockName } };
|
||||
});
|
||||
blockStatePalette[0] = { Name: { type: TagType.String, value: 'minecraft:air' } };
|
||||
|
||||
return blockStatePalette;
|
||||
}
|
||||
|
||||
private _convertToNBT(blockMesh: OtS_BlockMesh) {
|
||||
const bounds = blockMesh.getBounds();
|
||||
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
|
||||
|
||||
const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z;
|
||||
const blockMapping = this._createBlockMapping(blockMesh);
|
||||
|
||||
const blockStates = this._createBlockStates(blockMesh, blockMapping);
|
||||
const blockStatePalette = this._createBlockStatePalette(blockMapping);
|
||||
const numBlocks = blockMesh.getBlockCount();
|
||||
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'Litematic',
|
||||
value: {
|
||||
Metadata: {
|
||||
type: TagType.Compound, value: {
|
||||
Author: { type: TagType.String, value: '' },
|
||||
Description: { type: TagType.String, value: '' },
|
||||
Size: {
|
||||
type: TagType.Compound, value: {
|
||||
x: { type: TagType.Int, value: sizeVector.x },
|
||||
y: { type: TagType.Int, value: sizeVector.y },
|
||||
z: { type: TagType.Int, value: sizeVector.z },
|
||||
},
|
||||
},
|
||||
Name: { type: TagType.String, value: '' },
|
||||
RegionCount: { type: TagType.Int, value: 1 },
|
||||
TimeCreated: { type: TagType.Long, value: [0, 0] },
|
||||
TimeModified: { type: TagType.Long, value: [0, 0] },
|
||||
TotalBlocks: { type: TagType.Int, value: numBlocks },
|
||||
TotalVolume: { type: TagType.Int, value: bufferSize },
|
||||
},
|
||||
},
|
||||
Regions: {
|
||||
type: TagType.Compound, value: {
|
||||
Unnamed: {
|
||||
type: TagType.Compound, value: {
|
||||
BlockStates: { type: TagType.LongArray, value: blockStates },
|
||||
PendingBlockTicks: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
Position: {
|
||||
type: TagType.Compound, value: {
|
||||
x: { type: TagType.Int, value: 0 },
|
||||
y: { type: TagType.Int, value: 0 },
|
||||
z: { type: TagType.Int, value: 0 },
|
||||
},
|
||||
},
|
||||
BlockStatePalette: { type: TagType.List, value: { type: TagType.Compound, value: blockStatePalette } },
|
||||
Size: {
|
||||
type: TagType.Compound, value: {
|
||||
x: { type: TagType.Int, value: sizeVector.x },
|
||||
y: { type: TagType.Int, value: sizeVector.y },
|
||||
z: { type: TagType.Int, value: sizeVector.z },
|
||||
},
|
||||
},
|
||||
PendingFluidTicks: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
TileEntities: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
Entities: { type: TagType.List, value: { type: TagType.Int, value: [] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MinecraftDataVersion: { type: TagType.Int, value: AppConstants.DATA_VERSION },
|
||||
Version: { type: TagType.Int, value: 5 },
|
||||
},
|
||||
};
|
||||
|
||||
return nbt;
|
||||
}
|
||||
}
|
@ -1,130 +1,127 @@
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { AppConstants } from '../constants';
|
||||
import { LOC } from '../localiser';
|
||||
import { StatusHandler } from '../status';
|
||||
import { AppUtil } from '../util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport, TStructureRegion } from './base_exporter';
|
||||
import { Bounds } from '../bounds';
|
||||
import { ASSERT } from '../util/error_util';
|
||||
|
||||
export class NBTExporter extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Structure Blocks',
|
||||
extension: 'nbt',
|
||||
};
|
||||
}
|
||||
|
||||
private _processChunk(blockMesh: BlockMesh, min: Vector3, blockNameToIndex: Map<string, number>, palette: any): Buffer {
|
||||
const blocks: any[] = [];
|
||||
for (const block of blockMesh.getBlocks()) {
|
||||
const pos = block.voxel.position;
|
||||
const blockIndex = blockNameToIndex.get(block.blockInfo.name);
|
||||
|
||||
if (blockIndex !== undefined) {
|
||||
if (pos.x >= min.x && pos.x < min.x + 48 && pos.y >= min.y && pos.y < min.y + 48 && pos.z >= min.z && pos.z < min.z + 48) {
|
||||
const translatedPos = Vector3.sub(block.voxel.position, min);
|
||||
ASSERT(translatedPos.x >= 0 && translatedPos.x < 48);
|
||||
ASSERT(translatedPos.y >= 0 && translatedPos.y < 48);
|
||||
ASSERT(translatedPos.z >= 0 && translatedPos.z < 48);
|
||||
|
||||
blocks.push({
|
||||
pos: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Int,
|
||||
value: translatedPos.toArray(),
|
||||
},
|
||||
},
|
||||
state: {
|
||||
type: TagType.Int,
|
||||
value: blockIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT(blocks.length < 48 * 48 * 48);
|
||||
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'SchematicBlocks',
|
||||
value: {
|
||||
DataVersion: {
|
||||
type: TagType.Int,
|
||||
value: AppConstants.DATA_VERSION,
|
||||
},
|
||||
size: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Int,
|
||||
value: [48, 48, 48],
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Compound,
|
||||
value: palette,
|
||||
},
|
||||
},
|
||||
blocks: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Compound,
|
||||
value: blocks,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return saveNBT(nbt);
|
||||
}
|
||||
|
||||
public override export(blockMesh: BlockMesh) {
|
||||
const bounds = blockMesh.getVoxelMesh().getBounds();
|
||||
/*
|
||||
const sizeVector = bounds.getDimensions().add(1);
|
||||
|
||||
const isTooBig = sizeVector.x > 48 && sizeVector.y > 48 && sizeVector.z > 48;
|
||||
if (isTooBig) {
|
||||
StatusHandler.warning(LOC('export.nbt_exporter_too_big'));
|
||||
}
|
||||
*/
|
||||
|
||||
const blockNameToIndex = new Map<string, number>();
|
||||
const palette: any = [];
|
||||
for (const blockName of blockMesh.getBlockPalette()) {
|
||||
palette.push({
|
||||
Name: {
|
||||
type: TagType.String,
|
||||
value: AppUtil.Text.namespaceBlock(blockName),
|
||||
},
|
||||
});
|
||||
blockNameToIndex.set(blockName, palette.length - 1);
|
||||
}
|
||||
|
||||
const regions: TStructureRegion[] = [];
|
||||
|
||||
for (let x = bounds.min.x; x < bounds.max.x; x += 48) {
|
||||
for (let y = bounds.min.y; y < bounds.max.y; y += 48) {
|
||||
for (let z = bounds.min.z; z < bounds.max.z; z += 48) {
|
||||
const buffer = this._processChunk(blockMesh, new Vector3(x, y, z), blockNameToIndex, palette);
|
||||
regions.push({ content: buffer, name: `x${x - bounds.min.x}_y${y - bounds.min.y}_z${z - bounds.min.z}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const out: TStructureExport = {
|
||||
type: 'multiple',
|
||||
extension: '.nbt',
|
||||
regions: regions
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { AppConstants } from '../constants';
|
||||
import { AppUtil } from '../util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport, TStructureRegion } from './base_exporter';
|
||||
import { ASSERT } from '../util/error_util';
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
|
||||
export class NBTExporter extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Structure Blocks',
|
||||
extension: 'nbt',
|
||||
};
|
||||
}
|
||||
|
||||
private _processChunk(blockMesh: OtS_BlockMesh, min: Vector3, blockNameToIndex: Map<string, number>, palette: any): Uint8Array {
|
||||
const blocks: any[] = [];
|
||||
for (const { position, name } of blockMesh.getBlocks()) {
|
||||
const blockIndex = blockNameToIndex.get(name);
|
||||
|
||||
if (blockIndex !== undefined) {
|
||||
if (position.x >= min.x && position.x < min.x + 48 && position.y >= min.y && position.y < min.y + 48 && position.z >= min.z && position.z < min.z + 48) {
|
||||
const translatedPos = Vector3.sub(position, min);
|
||||
ASSERT(translatedPos.x >= 0 && translatedPos.x < 48);
|
||||
ASSERT(translatedPos.y >= 0 && translatedPos.y < 48);
|
||||
ASSERT(translatedPos.z >= 0 && translatedPos.z < 48);
|
||||
|
||||
blocks.push({
|
||||
pos: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Int,
|
||||
value: translatedPos.toArray(),
|
||||
},
|
||||
},
|
||||
state: {
|
||||
type: TagType.Int,
|
||||
value: blockIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT(blocks.length < 48 * 48 * 48);
|
||||
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'SchematicBlocks',
|
||||
value: {
|
||||
DataVersion: {
|
||||
type: TagType.Int,
|
||||
value: AppConstants.DATA_VERSION,
|
||||
},
|
||||
size: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Int,
|
||||
value: [48, 48, 48],
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Compound,
|
||||
value: palette,
|
||||
},
|
||||
},
|
||||
blocks: {
|
||||
type: TagType.List,
|
||||
value: {
|
||||
type: TagType.Compound,
|
||||
value: blocks,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return saveNBT(nbt);
|
||||
}
|
||||
|
||||
public override export(blockMesh: OtS_BlockMesh) {
|
||||
const bounds = blockMesh.getBounds();
|
||||
/*
|
||||
const sizeVector = bounds.getDimensions().add(1);
|
||||
|
||||
const isTooBig = sizeVector.x > 48 && sizeVector.y > 48 && sizeVector.z > 48;
|
||||
if (isTooBig) {
|
||||
StatusHandler.warning(LOC('export.nbt_exporter_too_big'));
|
||||
}
|
||||
*/
|
||||
|
||||
const blockNameToIndex = new Map<string, number>();
|
||||
const palette: any = [];
|
||||
for (const blockName of blockMesh.calcBlocksUsed()) {
|
||||
palette.push({
|
||||
Name: {
|
||||
type: TagType.String,
|
||||
value: AppUtil.Text.namespaceBlock(blockName),
|
||||
},
|
||||
});
|
||||
blockNameToIndex.set(blockName, palette.length - 1);
|
||||
}
|
||||
|
||||
const regions: TStructureRegion[] = [];
|
||||
|
||||
for (let x = bounds.min.x; x < bounds.max.x; x += 48) {
|
||||
for (let y = bounds.min.y; y < bounds.max.y; y += 48) {
|
||||
for (let z = bounds.min.z; z < bounds.max.z; z += 48) {
|
||||
const buffer = this._processChunk(blockMesh, new Vector3(x, y, z), blockNameToIndex, palette);
|
||||
regions.push({ content: buffer, name: `x${x - bounds.min.x}_y${y - bounds.min.y}_z${z - bounds.min.z}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const out: TStructureExport = {
|
||||
type: 'multiple',
|
||||
extension: '.nbt',
|
||||
regions: regions
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
@ -1,86 +1,87 @@
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { AppConstants } from '../constants';
|
||||
import { AppUtil } from '../util';
|
||||
import { LOG } from '../util/log_util';
|
||||
import { MathUtil } from '../util/math_util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
|
||||
export class SchemExporter extends IExporter {
|
||||
private static SCHEMA_VERSION = 2;
|
||||
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Sponge Schematic',
|
||||
extension: 'schem',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: BlockMesh): TStructureExport {
|
||||
const bounds = blockMesh.getVoxelMesh().getBounds();
|
||||
const sizeVector = bounds.getDimensions().add(1);
|
||||
|
||||
// https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-3.md#paletteObject
|
||||
// const blockMapping: BlockMapping = {};
|
||||
const blockMapping: {[name: string]: { type: TagType, value: any }} = {
|
||||
'minecraft:air': { type: TagType.Int, value: 0 },
|
||||
};
|
||||
|
||||
let blockIndex = 1;
|
||||
for (const blockName of blockMesh.getBlockPalette()) {
|
||||
const namespacedBlockName = AppUtil.Text.namespaceBlock(blockName);
|
||||
|
||||
blockMapping[namespacedBlockName] = { type: TagType.Int, value: blockIndex };
|
||||
++blockIndex;
|
||||
}
|
||||
LOG(blockMapping);
|
||||
|
||||
// const paletteObject = SchemExporter._createBlockStatePalette(blockMapping);
|
||||
const blockData = new Array<number>(sizeVector.x * sizeVector.y * sizeVector.z).fill(0);
|
||||
for (const block of blockMesh.getBlocks()) {
|
||||
const indexVector = Vector3.sub(block.voxel.position, bounds.min);
|
||||
const bufferIndex = SchemExporter._getBufferIndex(sizeVector, indexVector);
|
||||
const namespacedBlockName = AppUtil.Text.namespaceBlock(block.blockInfo.name);
|
||||
blockData[bufferIndex] = blockMapping[namespacedBlockName].value;
|
||||
}
|
||||
|
||||
const blockEncoding: number[] = [];
|
||||
for (let i = 0; i < blockData.length; ++i) {
|
||||
let id = blockData[i];
|
||||
|
||||
while ((id & -128) != 0) {
|
||||
blockEncoding.push(id & 127 | 128);
|
||||
id >>>= 7;
|
||||
}
|
||||
blockEncoding.push(id);
|
||||
}
|
||||
|
||||
for (let i = 0; i < blockEncoding.length; ++i) {
|
||||
blockEncoding[i] = MathUtil.int8(blockEncoding[i]);
|
||||
}
|
||||
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'Schematic',
|
||||
value: {
|
||||
Version: { type: TagType.Int, value: SchemExporter.SCHEMA_VERSION },
|
||||
DataVersion: { type: TagType.Int, value: AppConstants.DATA_VERSION },
|
||||
Width: { type: TagType.Short, value: sizeVector.x },
|
||||
Height: { type: TagType.Short, value: sizeVector.y },
|
||||
Length: { type: TagType.Short, value: sizeVector.z },
|
||||
PaletteMax: { type: TagType.Int, value: blockIndex },
|
||||
Palette: { type: TagType.Compound, value: blockMapping },
|
||||
BlockData: { type: TagType.ByteArray, value: blockEncoding },
|
||||
},
|
||||
};
|
||||
|
||||
return { type: 'single', extension: '.schem', content: saveNBT(nbt) };
|
||||
}
|
||||
|
||||
private static _getBufferIndex(dimensions: Vector3, vec: Vector3) {
|
||||
return vec.x + (vec.z * dimensions.x) + (vec.y * dimensions.x * dimensions.z);
|
||||
}
|
||||
}
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { AppConstants } from '../constants';
|
||||
import { AppUtil } from '../util';
|
||||
import { LOG } from '../util/log_util';
|
||||
import { MathUtil } from '../util/math_util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
|
||||
export class SchemExporter extends IExporter {
|
||||
private static SCHEMA_VERSION = 2;
|
||||
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Sponge Schematic',
|
||||
extension: 'schem',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: OtS_BlockMesh): TStructureExport {
|
||||
const bounds = blockMesh.getBounds();
|
||||
const sizeVector = bounds.getDimensions().add(1);
|
||||
|
||||
// https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-3.md#paletteObject
|
||||
// const blockMapping: BlockMapping = {};
|
||||
const blockMapping: {[name: string]: { type: TagType, value: any }} = {
|
||||
'minecraft:air': { type: TagType.Int, value: 0 },
|
||||
};
|
||||
|
||||
let blockIndex = 1;
|
||||
for (const blockName of blockMesh.calcBlocksUsed()) {
|
||||
const namespacedBlockName = AppUtil.Text.namespaceBlock(blockName);
|
||||
|
||||
blockMapping[namespacedBlockName] = { type: TagType.Int, value: blockIndex };
|
||||
++blockIndex;
|
||||
}
|
||||
LOG(blockMapping);
|
||||
|
||||
// const paletteObject = SchemExporter._createBlockStatePalette(blockMapping);
|
||||
const blockData = new Array<number>(sizeVector.x * sizeVector.y * sizeVector.z).fill(0);
|
||||
for (const { position, name } of blockMesh.getBlocks()) {
|
||||
const indexVector = Vector3.sub(position, bounds.min);
|
||||
const bufferIndex = SchemExporter._getBufferIndex(sizeVector, indexVector);
|
||||
const namespacedBlockName = AppUtil.Text.namespaceBlock(name);
|
||||
blockData[bufferIndex] = blockMapping[namespacedBlockName].value;
|
||||
}
|
||||
|
||||
const blockEncoding: number[] = [];
|
||||
for (let i = 0; i < blockData.length; ++i) {
|
||||
let id = blockData[i];
|
||||
|
||||
while ((id & -128) != 0) {
|
||||
blockEncoding.push(id & 127 | 128);
|
||||
id >>>= 7;
|
||||
}
|
||||
blockEncoding.push(id);
|
||||
}
|
||||
|
||||
for (let i = 0; i < blockEncoding.length; ++i) {
|
||||
blockEncoding[i] = MathUtil.int8(blockEncoding[i]);
|
||||
}
|
||||
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'Schematic',
|
||||
value: {
|
||||
Version: { type: TagType.Int, value: SchemExporter.SCHEMA_VERSION },
|
||||
DataVersion: { type: TagType.Int, value: AppConstants.DATA_VERSION },
|
||||
Width: { type: TagType.Short, value: sizeVector.x },
|
||||
Height: { type: TagType.Short, value: sizeVector.y },
|
||||
Length: { type: TagType.Short, value: sizeVector.z },
|
||||
PaletteMax: { type: TagType.Int, value: blockIndex },
|
||||
Palette: { type: TagType.Compound, value: blockMapping },
|
||||
BlockData: { type: TagType.ByteArray, value: blockEncoding },
|
||||
},
|
||||
};
|
||||
|
||||
return { type: 'single', extension: '.schem', content: saveNBT(nbt) };
|
||||
}
|
||||
|
||||
private static _getBufferIndex(dimensions: Vector3, vec: Vector3) {
|
||||
return vec.x + (vec.z * dimensions.x) + (vec.y * dimensions.x * dimensions.z);
|
||||
}
|
||||
}
|
@ -1,83 +1,82 @@
|
||||
//import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BLOCK_IDS } from '../../res/block_ids';
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { LOC } from '../localiser';
|
||||
import { StatusHandler } from '../status';
|
||||
import { LOG_WARN } from '../util/log_util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
|
||||
export class Schematic extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Schematic',
|
||||
extension: 'schematic',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: BlockMesh): TStructureExport {
|
||||
const nbt = this._convertToNBT(blockMesh);
|
||||
return { type: 'single', extension: '.schematic', content: saveNBT(nbt) };
|
||||
}
|
||||
|
||||
private _convertToNBT(blockMesh: BlockMesh): NBT {
|
||||
const bounds = blockMesh.getVoxelMesh().getBounds();
|
||||
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
|
||||
|
||||
const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z;
|
||||
const blocksData = Array<number>(bufferSize);
|
||||
const metaData = Array<number>(bufferSize);
|
||||
|
||||
// TODO Unimplemented
|
||||
const schematicBlocks: { [blockName: string]: { id: number, meta: number, name: string } } = BLOCK_IDS;
|
||||
|
||||
const blocks = blockMesh.getBlocks();
|
||||
const unsupportedBlocks = new Set<string>();
|
||||
let numBlocksUnsupported = 0;
|
||||
for (const block of blocks) {
|
||||
const indexVector = Vector3.sub(block.voxel.position, bounds.min);
|
||||
const index = this._getBufferIndex(indexVector, sizeVector);
|
||||
if (block.blockInfo.name in schematicBlocks) {
|
||||
const schematicBlock = schematicBlocks[block.blockInfo.name];
|
||||
blocksData[index] = new Int8Array([schematicBlock.id])[0];
|
||||
metaData[index] = new Int8Array([schematicBlock.meta])[0];
|
||||
} else {
|
||||
blocksData[index] = 1; // Default to a Stone block
|
||||
metaData[index] = 0;
|
||||
unsupportedBlocks.add(block.blockInfo.name);
|
||||
++numBlocksUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
if (unsupportedBlocks.size > 0) {
|
||||
StatusHandler.warning(LOC('export.schematic_unsupported_blocks', { count: numBlocksUnsupported, unique: unsupportedBlocks.size }));
|
||||
LOG_WARN(unsupportedBlocks);
|
||||
}
|
||||
|
||||
// TODO Unimplemented
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'Schematic',
|
||||
value: {
|
||||
Width: { type: TagType.Short, value: sizeVector.x },
|
||||
Height: { type: TagType.Short, value: sizeVector.y },
|
||||
Length: { type: TagType.Short, value: sizeVector.z },
|
||||
Materials: { type: TagType.String, value: 'Alpha' },
|
||||
Blocks: { type: TagType.ByteArray, value: blocksData },
|
||||
Data: { type: TagType.ByteArray, value: metaData },
|
||||
Entities: { type: TagType.List, value: { type: TagType.Int, value: Array(0) } },
|
||||
TileEntities: { type: TagType.List, value: { type: TagType.Int, value: Array(0) } },
|
||||
},
|
||||
};
|
||||
|
||||
return nbt;
|
||||
}
|
||||
|
||||
private _getBufferIndex(vec: Vector3, sizeVector: Vector3) {
|
||||
return (sizeVector.z * sizeVector.x * vec.y) + (sizeVector.x * vec.z) + vec.x;
|
||||
}
|
||||
}
|
||||
import { NBT, TagType } from 'prismarine-nbt';
|
||||
|
||||
import { BLOCK_IDS } from '../../../Editor/res/block_ids';
|
||||
import { BlockMesh } from '../block_mesh';
|
||||
import { LOG_WARN } from '../util/log_util';
|
||||
import { saveNBT } from '../util/nbt_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
|
||||
export class Schematic extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Schematic',
|
||||
extension: 'schematic',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: OtS_BlockMesh): TStructureExport {
|
||||
const nbt = this._convertToNBT(blockMesh);
|
||||
return { type: 'single', extension: '.schematic', content: saveNBT(nbt) };
|
||||
}
|
||||
|
||||
private _convertToNBT(blockMesh: OtS_BlockMesh): NBT {
|
||||
const bounds = blockMesh.getBounds();
|
||||
const sizeVector = Vector3.sub(bounds.max, bounds.min).add(1);
|
||||
|
||||
const bufferSize = sizeVector.x * sizeVector.y * sizeVector.z;
|
||||
const blocksData = Array<number>(bufferSize);
|
||||
const metaData = Array<number>(bufferSize);
|
||||
|
||||
// TODO Unimplemented
|
||||
const schematicBlocks: { [blockName: string]: { id: number, meta: number, name: string } } = BLOCK_IDS;
|
||||
|
||||
const unsupportedBlocks = new Set<string>();
|
||||
let numBlocksUnsupported = 0;
|
||||
for (const { position, name } of blockMesh.getBlocks()) {
|
||||
const indexVector = Vector3.sub(position, bounds.min);
|
||||
const index = this._getBufferIndex(indexVector, sizeVector);
|
||||
if (name in schematicBlocks) {
|
||||
const schematicBlock = schematicBlocks[name];
|
||||
blocksData[index] = new Int8Array([schematicBlock.id])[0];
|
||||
metaData[index] = new Int8Array([schematicBlock.meta])[0];
|
||||
} else {
|
||||
blocksData[index] = 1; // Default to a Stone block
|
||||
metaData[index] = 0;
|
||||
unsupportedBlocks.add(name);
|
||||
++numBlocksUnsupported;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: StatusRework
|
||||
/*
|
||||
if (unsupportedBlocks.size > 0) {
|
||||
StatusHandler.warning(LOC('export.schematic_unsupported_blocks', { count: numBlocksUnsupported, unique: unsupportedBlocks.size }));
|
||||
LOG_WARN(unsupportedBlocks);
|
||||
}
|
||||
*/
|
||||
|
||||
// TODO Unimplemented
|
||||
const nbt: NBT = {
|
||||
type: TagType.Compound,
|
||||
name: 'Schematic',
|
||||
value: {
|
||||
Width: { type: TagType.Short, value: sizeVector.x },
|
||||
Height: { type: TagType.Short, value: sizeVector.y },
|
||||
Length: { type: TagType.Short, value: sizeVector.z },
|
||||
Materials: { type: TagType.String, value: 'Alpha' },
|
||||
Blocks: { type: TagType.ByteArray, value: blocksData },
|
||||
Data: { type: TagType.ByteArray, value: metaData },
|
||||
Entities: { type: TagType.List, value: { type: TagType.Int, value: Array(0) } },
|
||||
TileEntities: { type: TagType.List, value: { type: TagType.Int, value: Array(0) } },
|
||||
},
|
||||
};
|
||||
|
||||
return nbt;
|
||||
}
|
||||
|
||||
private _getBufferIndex(vec: Vector3, sizeVector: Vector3) {
|
||||
return (sizeVector.z * sizeVector.x * vec.y) + (sizeVector.x * vec.z) + vec.x;
|
||||
}
|
||||
}
|
39
Core/src/exporters/uncompressed_json_exporter.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { OtS_BlockMesh } from '../ots_block_mesh';
|
||||
import { IExporter, TStructureExport } from './base_exporter';
|
||||
|
||||
export class UncompressedJSONExporter extends IExporter {
|
||||
public override getFormatFilter() {
|
||||
return {
|
||||
name: 'Uncompressed JSON',
|
||||
extension: 'json',
|
||||
};
|
||||
}
|
||||
|
||||
public override export(blockMesh: OtS_BlockMesh): TStructureExport {
|
||||
const blocks = blockMesh.getBlocks();
|
||||
|
||||
const lines = new Array<string>();
|
||||
lines.push('[');
|
||||
{
|
||||
// Serialise all block except for the last one.
|
||||
for (const { name, position } of blockMesh.getBlocks()) {
|
||||
lines.push(`{ "x": ${position.x}, "y": ${position.y}, "z": ${position.z}, "block_name": "${name}" },`);
|
||||
}
|
||||
|
||||
// Update the last block to not include the comma at the end.
|
||||
{
|
||||
const lastIndex = lines.length - 1;
|
||||
const lastEntry = lines[lastIndex];
|
||||
lines[lastIndex] = lastEntry.slice(0, -1);
|
||||
}
|
||||
}
|
||||
lines.push(']');
|
||||
|
||||
const json = lines.join('');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const buffer = encoder.encode(json);
|
||||
|
||||
return { type: 'single', extension: '.json', content: buffer };
|
||||
}
|
||||
}
|
5
Core/src/importers/base_importer.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { OtS_Mesh } from '../ots_mesh';
|
||||
|
||||
export abstract class OtS_Importer {
|
||||
public abstract import(file: ReadableStream<Uint8Array>): Promise<OtS_Mesh>;
|
||||
}
|
@ -1,40 +1,54 @@
|
||||
import { parse } from '@loaders.gl/core';
|
||||
import { GLTFLoader } from '@loaders.gl/gltf';
|
||||
|
||||
import { RGBAColours, RGBAUtil } from '../colour';
|
||||
import { LOC } from '../localiser';
|
||||
import { MaterialMap, MaterialType, Mesh, Tri } from '../mesh';
|
||||
import { StatusHandler } from '../status';
|
||||
import { UV } from '../util';
|
||||
import { AppError } from '../util/error_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { IImporter } from './base_importer';
|
||||
import { OtS_Mesh } from '../ots_mesh';
|
||||
import { OtS_Texture } from '../ots_texture';
|
||||
import { OtS_Importer } from './base_importer';
|
||||
import { OtS_Colours } from '../colour';
|
||||
|
||||
export class GltfLoader extends IImporter {
|
||||
public override import(file: File): Promise<Mesh> {
|
||||
StatusHandler.warning(LOC('import.gltf_experimental'));
|
||||
export type OtS_GltfImporterError =
|
||||
| { type: 'failed-to-parse' }
|
||||
| { type: 'unsupported-image-format' };
|
||||
|
||||
return new Promise<Mesh>((resolve, reject) => {
|
||||
parse(file, GLTFLoader, { loadImages: true })
|
||||
.then((gltf: any) => {
|
||||
resolve(this._handleGLTF(gltf));
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
interface VertexIndices {
|
||||
v0: number;
|
||||
v1: number;
|
||||
v2: number;
|
||||
};
|
||||
|
||||
export interface Tri {
|
||||
positionIndices: VertexIndices;
|
||||
texcoordIndices?: VertexIndices;
|
||||
material: string;
|
||||
};
|
||||
|
||||
export class OtS_Importer_Gltf extends OtS_Importer {
|
||||
public override async import(file: ReadableStream<Uint8Array>): Promise<OtS_Mesh> {
|
||||
// TODO: StatusRework
|
||||
//StatusHandler.warning(LOC('import.gltf_experimental'));
|
||||
|
||||
let gltf;
|
||||
try {
|
||||
gltf = await parse(file, GLTFLoader, { loadImages: true });
|
||||
} catch (err) {
|
||||
// TODO:
|
||||
//throw new GltfImporterError({ type: 'failed-to-parse' });
|
||||
}
|
||||
|
||||
return this._handleGLTF(gltf);
|
||||
}
|
||||
|
||||
private _handleGLTF(gltf: any): Mesh {
|
||||
private _handleGLTF(gltf: any): OtS_Mesh {
|
||||
const meshVertices: Vector3[] = [];
|
||||
const meshNormals: Vector3[] = [];
|
||||
//const meshNormals: Vector3[] = [];
|
||||
const meshTexcoords: UV[] = [];
|
||||
const meshTriangles: Tri[] = [];
|
||||
const meshMaterials: MaterialMap = new Map();
|
||||
const meshMaterials: Map<string, { }> = new Map();
|
||||
meshMaterials.set('NONE', {
|
||||
type: MaterialType.solid,
|
||||
colour: RGBAUtil.copy(RGBAColours.WHITE),
|
||||
needsAttention: false,
|
||||
type: 'solid',
|
||||
colour: OtS_Colours.WHITE,
|
||||
canBeTextured: false,
|
||||
});
|
||||
let maxIndex = 0;
|
||||
@ -52,6 +66,7 @@ export class GltfLoader extends IImporter {
|
||||
));
|
||||
}
|
||||
}
|
||||
/*
|
||||
if (attributes.NORMAL !== undefined) {
|
||||
const normals = attributes.NORMAL.value as Float32Array;
|
||||
for (let i = 0; i < normals.length; i += 3) {
|
||||
@ -62,13 +77,14 @@ export class GltfLoader extends IImporter {
|
||||
));
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (attributes.TEXCOORD_0 !== undefined) {
|
||||
const texcoords = attributes.TEXCOORD_0.value as Float32Array;
|
||||
for (let i = 0; i < texcoords.length; i += 2) {
|
||||
meshTexcoords.push(new UV(
|
||||
texcoords[i + 0],
|
||||
1.0 - texcoords[i + 1],
|
||||
));
|
||||
meshTexcoords.push({
|
||||
u: texcoords[i + 0],
|
||||
v: 1.0 - texcoords[i + 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
// Material
|
||||
@ -88,8 +104,9 @@ export class GltfLoader extends IImporter {
|
||||
|
||||
try {
|
||||
if (mimeType !== 'image/png' && mimeType !== 'image/jpeg') {
|
||||
StatusHandler.warning(LOC('import.unsupported_image_type', { file_name: diffuseTexture.texture.source.id, file_type: mimeType }));
|
||||
throw new Error('Unsupported image type');
|
||||
// TODO: StatusRework
|
||||
//StatusHandler.warning(LOC('import.unsupported_image_type', { file_name: diffuseTexture.texture.source.id, file_type: mimeType }));
|
||||
//throw new GltfImporterError({ type: 'unsupported-image-format' })
|
||||
}
|
||||
|
||||
const base64 = btoa(
|
||||
@ -97,21 +114,15 @@ export class GltfLoader extends IImporter {
|
||||
);
|
||||
|
||||
meshMaterials.set(materialName, {
|
||||
type: MaterialType.textured,
|
||||
diffuse: {
|
||||
filetype: mimeType === 'image/jpeg' ? 'jpg' : 'png',
|
||||
raw: (mimeType === 'image/jpeg' ? 'data:image/jpeg;base64,' : 'data:image/png;base64,') + base64,
|
||||
},
|
||||
extension: 'clamp',
|
||||
interpolation: 'linear',
|
||||
needsAttention: false,
|
||||
transparency: { type: 'None' },
|
||||
name: materialName,
|
||||
type: 'textured',
|
||||
texture: OtS_Texture.CreateDebugTexture(),
|
||||
});
|
||||
} catch {
|
||||
meshMaterials.set(materialName, {
|
||||
type: MaterialType.solid,
|
||||
colour: RGBAUtil.copy(RGBAColours.WHITE),
|
||||
needsAttention: false,
|
||||
name: materialName,
|
||||
type: 'solid',
|
||||
colour: OtS_Colours.WHITE,
|
||||
canBeTextured: true,
|
||||
});
|
||||
}
|
||||
@ -128,14 +139,14 @@ export class GltfLoader extends IImporter {
|
||||
|
||||
if (diffuseColour !== undefined) {
|
||||
meshMaterials.set(materialName, {
|
||||
type: MaterialType.solid,
|
||||
name: materialName,
|
||||
type: 'solid',
|
||||
colour: {
|
||||
r: diffuseColour[0],
|
||||
g: diffuseColour[1],
|
||||
b: diffuseColour[2],
|
||||
a: diffuseColour[3],
|
||||
},
|
||||
needsAttention: false,
|
||||
canBeTextured: false,
|
||||
});
|
||||
}
|
||||
@ -148,14 +159,14 @@ export class GltfLoader extends IImporter {
|
||||
const emissiveColour: (number[] | undefined) = primitive.material.pbr;
|
||||
if (!materialMade && emissiveColour !== undefined) {
|
||||
meshMaterials.set(materialName, {
|
||||
type: MaterialType.solid,
|
||||
name: materialName,
|
||||
type: 'solid',
|
||||
colour: {
|
||||
r: emissiveColour[0],
|
||||
g: emissiveColour[1],
|
||||
b: emissiveColour[2],
|
||||
a: 1.0,
|
||||
},
|
||||
needsAttention: false,
|
||||
canBeTextured: false,
|
||||
});
|
||||
|
||||
@ -171,14 +182,14 @@ export class GltfLoader extends IImporter {
|
||||
meshTriangles.push({
|
||||
material: materialNameToUse,
|
||||
positionIndices: {
|
||||
x: maxIndex + indices[i * 3 + 0],
|
||||
y: maxIndex + indices[i * 3 + 1],
|
||||
z: maxIndex + indices[i * 3 + 2],
|
||||
v0: maxIndex + indices[i * 3 + 0],
|
||||
v1: maxIndex + indices[i * 3 + 1],
|
||||
v2: maxIndex + indices[i * 3 + 2],
|
||||
},
|
||||
texcoordIndices: {
|
||||
x: maxIndex + indices[i * 3 + 0],
|
||||
y: maxIndex + indices[i * 3 + 1],
|
||||
z: maxIndex + indices[i * 3 + 2],
|
||||
v0: maxIndex + indices[i * 3 + 0],
|
||||
v1: maxIndex + indices[i * 3 + 1],
|
||||
v2: maxIndex + indices[i * 3 + 2],
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -193,12 +204,10 @@ export class GltfLoader extends IImporter {
|
||||
});
|
||||
});
|
||||
|
||||
return new Mesh(
|
||||
meshVertices,
|
||||
meshNormals,
|
||||
meshTexcoords,
|
||||
meshTriangles,
|
||||
meshMaterials,
|
||||
);
|
||||
const mesh = OtS_Mesh.create();
|
||||
|
||||
// TODO: Fix
|
||||
|
||||
return mesh;
|
||||
}
|
||||
}
|
16
Core/src/importers/importers.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { OtS_Importer } from './base_importer';
|
||||
import { OtS_Importer_Gltf } from './gltf_importer';
|
||||
import { OtS_Importer_Obj } from './obj_importer';
|
||||
|
||||
export type OtS_Importers = 'obj' | 'gltf';
|
||||
|
||||
export class OtS_ImporterFactory {
|
||||
public static GetImporter(importer: OtS_Importers): OtS_Importer {
|
||||
switch (importer) {
|
||||
case 'obj':
|
||||
return new OtS_Importer_Obj();
|
||||
case 'gltf':
|
||||
return new OtS_Importer_Gltf();
|
||||
}
|
||||
}
|
||||
}
|
422
Core/src/importers/obj_importer.ts
Normal file
@ -0,0 +1,422 @@
|
||||
import { OtS_Colours, RGBAUtil } from '../colour';
|
||||
import { OtS_Mesh } from '../ots_mesh';
|
||||
import { OtS_Texture } from '../ots_texture';
|
||||
import { Triangle } from '../triangle';
|
||||
import { UV } from '../util';
|
||||
import { ASSERT } from '../util/error_util';
|
||||
import { RegExpBuilder } from '../util/regex_util';
|
||||
import { REGEX_NZ_ANY } from '../util/regex_util';
|
||||
import { REGEX_NUMBER } from '../util/regex_util';
|
||||
import { Vector3 } from '../vector';
|
||||
import { OtS_Importer } from './base_importer';
|
||||
|
||||
export type OtS_ObjImporterError =
|
||||
| { type: 'invalid-encoding' }
|
||||
| { type: 'invalid-material-name', name: string }
|
||||
| { type: 'invalid-data' }
|
||||
| { type: 'failed-to-parse', line: string }
|
||||
| { type: 'failed-to-parse-essential-token', line: string };
|
||||
|
||||
interface VertexIndices {
|
||||
v0: number;
|
||||
v1: number;
|
||||
v2: number;
|
||||
}
|
||||
|
||||
export interface Tri {
|
||||
positionIndices: VertexIndices;
|
||||
normalIndices?: VertexIndices;
|
||||
texcoordIndices?: VertexIndices;
|
||||
material: string;
|
||||
}
|
||||
|
||||
export class OtS_Importer_Obj extends OtS_Importer {
|
||||
private _vertices: Vector3[] = [];
|
||||
private _normals: Vector3[] = [];
|
||||
private _uvs: UV[] = [];
|
||||
private _tris: Tri[] = [];
|
||||
private _currentMaterialName: string = 'DEFAULT_UNASSIGNED';
|
||||
// Parser context
|
||||
private _linesToParse: string[] = [];
|
||||
|
||||
private static _REGEX_USEMTL = new RegExpBuilder()
|
||||
.add(/^usemtl/)
|
||||
.add(/ /)
|
||||
.add(REGEX_NZ_ANY, 'name')
|
||||
.toRegExp();
|
||||
|
||||
private static _REGEX_VERTEX = new RegExpBuilder()
|
||||
.add(/^v/)
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'x')
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'y')
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'z')
|
||||
.toRegExp();
|
||||
|
||||
private static _REGEX_NORMAL = new RegExpBuilder()
|
||||
.add(/^vn/)
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'x')
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'y')
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'z')
|
||||
.toRegExp();
|
||||
|
||||
private static _REGEX_TEXCOORD = new RegExpBuilder()
|
||||
.add(/^vt/)
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'u')
|
||||
.addNonzeroWhitespace()
|
||||
.add(REGEX_NUMBER, 'v')
|
||||
.toRegExp();
|
||||
|
||||
private static _REGEX_FACE = new RegExpBuilder()
|
||||
.add(/^f/)
|
||||
.addNonzeroWhitespace()
|
||||
.add(/.*/, 'line')
|
||||
.toRegExp();
|
||||
|
||||
private _consumeLoadedLines(includeLast: boolean) {
|
||||
const size = includeLast ? this._linesToParse.length : this._linesToParse.length - 1;
|
||||
|
||||
for (let i = 0; i < size; ++i) {
|
||||
const line = this._linesToParse[i];
|
||||
const { err } = this.parseOBJLine(line);
|
||||
if (err !== null) {
|
||||
// TODO:
|
||||
//throw new ObjImporterError(err);
|
||||
}
|
||||
}
|
||||
|
||||
this._linesToParse.splice(0, size);
|
||||
}
|
||||
|
||||
public override async import(file: ReadableStream<Uint8Array>): Promise<OtS_Mesh> {
|
||||
const reader = file.getReader();
|
||||
const decoder = new TextDecoder(); //utf8
|
||||
|
||||
let lastChunkEndedWithNewline = false;
|
||||
let result: ReadableStreamReadResult<Uint8Array>;
|
||||
do {
|
||||
result = await reader.read();
|
||||
const string = decoder.decode(result.value);
|
||||
|
||||
const newLines = string.split(/\r?\n/);
|
||||
if (newLines.length > 0) {
|
||||
if (this._linesToParse.length === 0 || lastChunkEndedWithNewline) {
|
||||
this._linesToParse = this._linesToParse.concat(newLines);
|
||||
} else {
|
||||
const lastIndex = this._linesToParse.length - 1;
|
||||
const last = this._linesToParse[lastIndex];
|
||||
this._linesToParse[lastIndex] = last + newLines[0];
|
||||
this._linesToParse = this._linesToParse.concat(newLines.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
this._consumeLoadedLines(result.done);
|
||||
|
||||
const lastChar = string[string.length - 1];
|
||||
lastChunkEndedWithNewline = lastChar === '\n' || lastChar === '\r\n';
|
||||
} while (!result.done);
|
||||
|
||||
const materials = new Map<string, boolean>();
|
||||
this._tris.forEach((tri) => {
|
||||
if (!materials.has(tri.material)) {
|
||||
materials.set(tri.material, tri.texcoordIndices !== undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const mesh = OtS_Mesh.create();
|
||||
for (const [material, isTextureMaterial] of materials) {
|
||||
if (isTextureMaterial) {
|
||||
const positionData: number[] = [];
|
||||
const normalData: number[] = [];
|
||||
const texcoordData: number[] = [];
|
||||
const indexData: number[] = [];
|
||||
let ni = 0;
|
||||
|
||||
this._tris.forEach((tri) => {
|
||||
if (tri.material === material) {
|
||||
const p0 = this._vertices[tri.positionIndices.v0];
|
||||
const p1 = this._vertices[tri.positionIndices.v1];
|
||||
const p2 = this._vertices[tri.positionIndices.v2];
|
||||
|
||||
let n0: Vector3;
|
||||
let n1: Vector3;
|
||||
let n2: Vector3;
|
||||
if (tri.normalIndices) {
|
||||
n0 = this._normals[tri.normalIndices.v0];
|
||||
n1 = this._normals[tri.normalIndices.v1];
|
||||
n2 = this._normals[tri.normalIndices.v2];
|
||||
} else {
|
||||
n0 = Triangle.CalcNormal(p0, p1, p2);
|
||||
n1 = n0.copy();
|
||||
n2 = n0.copy();
|
||||
}
|
||||
|
||||
ASSERT(tri.texcoordIndices !== undefined);
|
||||
const t0 = this._uvs[tri.texcoordIndices.v0];
|
||||
const t1 = this._uvs[tri.texcoordIndices.v1];
|
||||
const t2 = this._uvs[tri.texcoordIndices.v2];
|
||||
|
||||
positionData.push(p0.x, p0.y, p0.z);
|
||||
normalData.push(n0.x, n0.y, n0.z);
|
||||
texcoordData.push(t0.u, t0.v);
|
||||
indexData.push(ni++);
|
||||
positionData.push(p1.x, p1.y, p1.z);
|
||||
normalData.push(n1.x, n1.y, n1.z);
|
||||
texcoordData.push(t1.u, t1.v);
|
||||
indexData.push(ni++);
|
||||
positionData.push(p2.x, p2.y, p2.z);
|
||||
normalData.push(n2.x, n2.y, n2.z);
|
||||
texcoordData.push(t2.u, t2.v);
|
||||
indexData.push(ni++);
|
||||
}
|
||||
});
|
||||
|
||||
mesh.addSection({
|
||||
name: material,
|
||||
type: 'textured',
|
||||
texture: OtS_Texture.CreateDebugTexture(),
|
||||
positionData: Float32Array.from(positionData),
|
||||
texcoordData: Float32Array.from(texcoordData),
|
||||
normalData: Float32Array.from(normalData),
|
||||
indexData: Uint32Array.from(indexData),
|
||||
});
|
||||
} else {
|
||||
const positionData: number[] = [];
|
||||
const normalData: number[] = [];
|
||||
const indexData: number[] = [];
|
||||
let ni = 0;
|
||||
|
||||
this._tris.forEach((tri) => {
|
||||
if (tri.material === material) {
|
||||
const p0 = this._vertices[tri.positionIndices.v0];
|
||||
const p1 = this._vertices[tri.positionIndices.v1];
|
||||
const p2 = this._vertices[tri.positionIndices.v2];
|
||||
|
||||
let n0: Vector3;
|
||||
let n1: Vector3;
|
||||
let n2: Vector3;
|
||||
if (tri.normalIndices) {
|
||||
n0 = this._normals[tri.normalIndices.v0];
|
||||
n1 = this._normals[tri.normalIndices.v1];
|
||||
n2 = this._normals[tri.normalIndices.v2];
|
||||
} else {
|
||||
n0 = Triangle.CalcNormal(p0, p1, p2);
|
||||
n1 = n0.copy();
|
||||
n2 = n0.copy();
|
||||
}
|
||||
|
||||
positionData.push(p0.x, p0.y, p0.z);
|
||||
normalData.push(n0.x, n0.y, n0.z);
|
||||
indexData.push(ni++);
|
||||
positionData.push(p1.x, p1.y, p1.z);
|
||||
normalData.push(n1.x, n1.y, n1.z);
|
||||
indexData.push(ni++);
|
||||
positionData.push(p2.x, p2.y, p2.z);
|
||||
normalData.push(n2.x, n2.y, n2.z);
|
||||
indexData.push(ni++);
|
||||
}
|
||||
});
|
||||
|
||||
mesh.addSection({
|
||||
name: material,
|
||||
type: 'solid',
|
||||
colour: OtS_Colours.WHITE,
|
||||
positionData: Float32Array.from(positionData),
|
||||
normalData: Float32Array.from(normalData),
|
||||
indexData: Uint32Array.from(indexData),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse the given line of an OBJ file.
|
||||
* Potentially returns an error if failed to do so.
|
||||
*/
|
||||
public parseOBJLine(line: string): { err: null | OtS_ObjImporterError } {
|
||||
false
|
||||
|| this._tryParseAsUsemtl(line)
|
||||
|| this._tryParseAsVertex(line)
|
||||
|| this._tryParseAsNormal(line)
|
||||
|| this._tryParseAsTexcoord(line)
|
||||
|| this._tryParseAsFace(line);
|
||||
|
||||
return { err: null };
|
||||
}
|
||||
|
||||
// e.g. 'usemtl my_material'
|
||||
private _tryParseAsUsemtl(line: string): boolean {
|
||||
const match = OtS_Importer_Obj._REGEX_USEMTL.exec(line);
|
||||
if (match === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const materialName = match.groups?.name.trim();
|
||||
|
||||
if (materialName === undefined || materialName.length === 0) {
|
||||
throw 'Invalid material name'; // TODO: Error type
|
||||
}
|
||||
|
||||
this._currentMaterialName = materialName;
|
||||
return true;
|
||||
}
|
||||
|
||||
// e.g. 'v 0.123 0.456 0.789'
|
||||
private _tryParseAsVertex(line: string): boolean {
|
||||
const match = OtS_Importer_Obj._REGEX_VERTEX.exec(line);
|
||||
if (match === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const x = parseFloat(match.groups?.x ?? '');
|
||||
const y = parseFloat(match.groups?.y ?? '');
|
||||
const z = parseFloat(match.groups?.z ?? '');
|
||||
|
||||
if (isNaN(x) || isNaN(y) || isNaN(z)) {
|
||||
throw 'Invalid data'; // TODO: Error type
|
||||
}
|
||||
|
||||
this._vertices.push(new Vector3(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
// e.g. 'vn 0.123 0.456 0.789'
|
||||
private _tryParseAsNormal(line: string): boolean {
|
||||
const match = OtS_Importer_Obj._REGEX_NORMAL.exec(line);
|
||||
if (match === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const x = parseFloat(match.groups?.x ?? '');
|
||||
const y = parseFloat(match.groups?.y ?? '');
|
||||
const z = parseFloat(match.groups?.z ?? '');
|
||||
|
||||
if (isNaN(x) || isNaN(y) || isNaN(z)) {
|
||||
throw 'Invalid data'; // TODO: Error type
|
||||
}
|
||||
|
||||
this._normals.push(new Vector3(x, y, z));
|
||||
return true;
|
||||
}
|
||||
|
||||
// e.g. 'vt 0.123 0.456'
|
||||
private _tryParseAsTexcoord(line: string): boolean {
|
||||
const match = OtS_Importer_Obj._REGEX_TEXCOORD.exec(line);
|
||||
if (match === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u = parseFloat(match.groups?.u ?? '');
|
||||
const v = parseFloat(match.groups?.v ?? '');
|
||||
|
||||
if (isNaN(u) || isNaN(v)) {
|
||||
throw 'Invalid data';
|
||||
}
|
||||
|
||||
this._uvs.push({ u: u, v: v });
|
||||
return true;
|
||||
}
|
||||
|
||||
private _tryParseAsFace(line: string): boolean {
|
||||
const match = OtS_Importer_Obj._REGEX_FACE.exec(line);
|
||||
if (match === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = match.groups?.line.trim();
|
||||
if (data === undefined) {
|
||||
throw 'Invalid data';
|
||||
}
|
||||
|
||||
const vertices = data.split(' ').filter((x) => {
|
||||
return x.length !== 0;
|
||||
});
|
||||
|
||||
if (vertices.length < 3) {
|
||||
throw 'Invalid data';
|
||||
}
|
||||
|
||||
const points: {
|
||||
positionIndex: number;
|
||||
normalIndex?: number;
|
||||
texcoordIndex?: number;
|
||||
}[] = [];
|
||||
|
||||
for (const vertex of vertices) {
|
||||
const vertexData = vertex.split('/');
|
||||
switch (vertexData.length) {
|
||||
case 1: {
|
||||
const index = parseInt(vertexData[0]);
|
||||
points.push({
|
||||
positionIndex: index,
|
||||
normalIndex: index,
|
||||
texcoordIndex: index,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
const positionIndex = parseInt(vertexData[0]);
|
||||
const texcoordIndex = parseInt(vertexData[1]);
|
||||
points.push({
|
||||
positionIndex: positionIndex,
|
||||
texcoordIndex: texcoordIndex,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
const positionIndex = parseInt(vertexData[0]);
|
||||
const texcoordIndex = parseInt(vertexData[1]);
|
||||
const normalIndex = parseInt(vertexData[2]);
|
||||
points.push({
|
||||
positionIndex: positionIndex,
|
||||
texcoordIndex: texcoordIndex,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw 'Invalid data';
|
||||
}
|
||||
}
|
||||
|
||||
const pointBase = points[0];
|
||||
for (let i = 1; i < points.length - 1; ++i) {
|
||||
const pointA = points[i];
|
||||
const pointB = points[i + 1];
|
||||
const tri: Tri = {
|
||||
positionIndices: {
|
||||
v0: pointBase.positionIndex - 1,
|
||||
v1: pointA.positionIndex - 1,
|
||||
v2: pointB.positionIndex - 1,
|
||||
},
|
||||
material: this._currentMaterialName,
|
||||
};
|
||||
if (pointBase.normalIndex || pointA.normalIndex || pointB.normalIndex) {
|
||||
ASSERT(pointBase.normalIndex && pointA.normalIndex && pointB.normalIndex);
|
||||
tri.normalIndices = {
|
||||
v0: pointBase.normalIndex - 1,
|
||||
v1: pointA.normalIndex - 1,
|
||||
v2: pointB.normalIndex - 1,
|
||||
};
|
||||
}
|
||||
if (pointBase.texcoordIndex || pointA.texcoordIndex || pointB.texcoordIndex) {
|
||||
ASSERT(pointBase.texcoordIndex && pointA.texcoordIndex && pointB.texcoordIndex);
|
||||
tri.texcoordIndices = {
|
||||
v0: pointBase.texcoordIndex - 1,
|
||||
v1: pointA.texcoordIndex - 1,
|
||||
v2: pointB.texcoordIndex - 1,
|
||||
};
|
||||
}
|
||||
this._tris.push(tri);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -73,26 +73,25 @@ export class BlockMeshLighting {
|
||||
|
||||
if (this.getMaxLightLevel(potentialBlockPos) < threshold) {
|
||||
const success = this._owner.setEmissiveBlock(potentialBlockPos);
|
||||
ASSERT(success);
|
||||
|
||||
if (success) {
|
||||
const newBlockLight = 14; // TODO: Not necessarily 14
|
||||
this._blockLightValues.set(potentialBlockPos.hash(), newBlockLight);
|
||||
const newBlockLight = 14; // TODO: Not necessarily 14
|
||||
this._blockLightValues.set(potentialBlockPos.hash(), newBlockLight);
|
||||
|
||||
const attenuated: TLightLevel = {
|
||||
sunLightValue: this.getLightLevel(potentialBlockPos).sunLightValue - 1,
|
||||
blockLightValue: newBlockLight - 1,
|
||||
};
|
||||
const updates: TLightUpdate[] = [];
|
||||
updates.push({ pos: new Vector3(0, 1, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.Down });
|
||||
updates.push({ pos: new Vector3(0, -1, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.Up });
|
||||
updates.push({ pos: new Vector3(1, 0, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.South });
|
||||
updates.push({ pos: new Vector3(-1, 0, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.North });
|
||||
updates.push({ pos: new Vector3(0, 0, 1).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.West });
|
||||
updates.push({ pos: new Vector3(0, 0, -1).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.East });
|
||||
//this._handleUpdates(updates, false, true);
|
||||
this._handleBlockLightUpdates(updates);
|
||||
ASSERT(updates.length === 0);
|
||||
}
|
||||
const attenuated: TLightLevel = {
|
||||
sunLightValue: this.getLightLevel(potentialBlockPos).sunLightValue - 1,
|
||||
blockLightValue: newBlockLight - 1,
|
||||
};
|
||||
const updates: TLightUpdate[] = [];
|
||||
updates.push({ pos: new Vector3(0, 1, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.Down });
|
||||
updates.push({ pos: new Vector3(0, -1, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.Up });
|
||||
updates.push({ pos: new Vector3(1, 0, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.South });
|
||||
updates.push({ pos: new Vector3(-1, 0, 0).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.North });
|
||||
updates.push({ pos: new Vector3(0, 0, 1).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.West });
|
||||
updates.push({ pos: new Vector3(0, 0, -1).add(potentialBlockPos), sunLightValue: attenuated.sunLightValue, blockLightValue: attenuated.blockLightValue, from: EFace.East });
|
||||
//this._handleUpdates(updates, false, true);
|
||||
this._handleBlockLightUpdates(updates);
|
||||
ASSERT(updates.length === 0);
|
||||
}
|
||||
}
|
||||
}
|
43
Core/src/materials.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { RGBA, RGBAUtil } from './colour';
|
||||
import { OtS_Texture } from './ots_texture';
|
||||
|
||||
// export type OtS_MaterialType = 'solid' | 'textured';
|
||||
|
||||
// type BaseMaterial = {
|
||||
// //name: string,
|
||||
// }
|
||||
|
||||
// export type SolidMaterial = BaseMaterial & {
|
||||
// type: 'solid'
|
||||
// //canBeTextured: boolean,
|
||||
// }
|
||||
// export type TexturedMaterial = BaseMaterial & {
|
||||
// type: 'textured',
|
||||
// texture: OtS_Texture,
|
||||
// }
|
||||
|
||||
// export type Material = SolidMaterial | TexturedMaterial;
|
||||
|
||||
// export namespace OtS_Util {
|
||||
// export function copySolidMaterial(material: SolidMaterial): SolidMaterial {
|
||||
// return {
|
||||
// type: 'solid',
|
||||
// colour: RGBAUtil.copy(material.colour),
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function copyTexturedMaterial(material: TexturedMaterial): TexturedMaterial {
|
||||
// return {
|
||||
// type: 'textured',
|
||||
// texture: material.texture.copy(),
|
||||
// }
|
||||
// }
|
||||
|
||||
// export function copyMaterial(material: Material): Material {
|
||||
// if (material.type === 'solid') {
|
||||
// return OtS_Util.copySolidMaterial(material);
|
||||
// } else {
|
||||
// return OtS_Util.copyTexturedMaterial(material);
|
||||
// }
|
||||
// }
|
||||
// }
|
@ -1,155 +1,154 @@
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
|
||||
export namespace AppMath {
|
||||
export const RADIANS_0 = degreesToRadians(0.0);
|
||||
export const RADIANS_90 = degreesToRadians(90.0);
|
||||
export const RADIANS_180 = degreesToRadians(180.0);
|
||||
export const RADIANS_270 = degreesToRadians(270.0);
|
||||
|
||||
export function lerp(value: number, start: number, end: number) {
|
||||
return (1 - value) * start + value * end;
|
||||
}
|
||||
|
||||
export function nearlyEqual(a: number, b: number, tolerance: number = 0.0001) {
|
||||
return Math.abs(a - b) < tolerance;
|
||||
}
|
||||
|
||||
export function degreesToRadians(degrees: number) {
|
||||
return degrees * (Math.PI / 180.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a float in [0, 1] to an int in [0, 255]
|
||||
* @param decimal A number in [0, 1]
|
||||
*/
|
||||
export function uint8(decimal: number) {
|
||||
return Math.floor(decimal * 255);
|
||||
}
|
||||
|
||||
export function largestPowerOfTwoLessThanN(n: number) {
|
||||
return Math.floor(Math.log2(n));
|
||||
}
|
||||
}
|
||||
|
||||
export const argMax = (array: [number]) => {
|
||||
return array.map((x, i) => [x, i]).reduce((r, a) => (a[0] > r[0] ? a : r))[1];
|
||||
};
|
||||
|
||||
export const clamp = (value: number, min: number, max: number) => {
|
||||
return Math.max(Math.min(max, value), min);
|
||||
};
|
||||
|
||||
export const floorToNearest = (value: number, base: number) => {
|
||||
return Math.floor(value / base) * base;
|
||||
};
|
||||
|
||||
export const ceilToNearest = (value: number, base: number) => {
|
||||
return Math.ceil(value / base) * base;
|
||||
};
|
||||
|
||||
export const roundToNearest = (value: number, base: number) => {
|
||||
return Math.round(value / base) * base;
|
||||
};
|
||||
|
||||
export const between = (value: number, min: number, max: number) => {
|
||||
return min <= value && value <= max;
|
||||
};
|
||||
|
||||
export const mapRange = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin;
|
||||
};
|
||||
|
||||
export const wayThrough = (value: number, min: number, max: number) => {
|
||||
// ASSERT(value >= min && value <= max);
|
||||
return (value - min) / (max - min);
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws is any number in args is NaN
|
||||
*/
|
||||
export const checkNaN = (...args: number[]) => {
|
||||
const existsNaN = args.some((arg) => {
|
||||
return isNaN(arg);
|
||||
});
|
||||
ASSERT(!existsNaN, 'Found NaN');
|
||||
};
|
||||
|
||||
export const degreesToRadians = Math.PI / 180;
|
||||
|
||||
export class SmoothVariable {
|
||||
private _actual: number;
|
||||
private _target: number;
|
||||
private _smoothing: number;
|
||||
private _min: number;
|
||||
private _max: number;
|
||||
|
||||
public constructor(value: number, smoothing: number) {
|
||||
this._actual = value;
|
||||
this._target = value;
|
||||
this._smoothing = smoothing;
|
||||
this._min = -Infinity;
|
||||
this._max = Infinity;
|
||||
}
|
||||
|
||||
public setClamp(min: number, max: number) {
|
||||
this._min = min;
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
public addToTarget(delta: number) {
|
||||
this._target = clamp(this._target + delta, this._min, this._max);
|
||||
}
|
||||
|
||||
public setTarget(target: number) {
|
||||
this._target = clamp(target, this._min, this._max);
|
||||
}
|
||||
|
||||
public setActual(actual: number) {
|
||||
this._actual = actual;
|
||||
}
|
||||
|
||||
public tick() {
|
||||
this._actual += (this._target - this._actual) * this._smoothing;
|
||||
}
|
||||
|
||||
public getActual() {
|
||||
return this._actual;
|
||||
}
|
||||
|
||||
public getTarget() {
|
||||
return this._target;
|
||||
}
|
||||
}
|
||||
|
||||
export class SmoothVectorVariable {
|
||||
private _actual: Vector3;
|
||||
private _target: Vector3;
|
||||
private _smoothing: number;
|
||||
|
||||
public constructor(value: Vector3, smoothing: number) {
|
||||
this._actual = value;
|
||||
this._target = value;
|
||||
this._smoothing = smoothing;
|
||||
}
|
||||
|
||||
public addToTarget(delta: Vector3) {
|
||||
this._target = Vector3.add(this._target, delta);
|
||||
}
|
||||
|
||||
public setTarget(target: Vector3) {
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
public tick() {
|
||||
this._actual.add(Vector3.sub(this._target, this._actual).mulScalar(this._smoothing));
|
||||
}
|
||||
|
||||
public getActual() {
|
||||
return this._actual;
|
||||
}
|
||||
|
||||
public getTarget() {
|
||||
return this._target;
|
||||
}
|
||||
}
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
|
||||
export namespace AppMath {
|
||||
export const RADIANS_0 = degreesToRadians(0.0);
|
||||
export const RADIANS_90 = degreesToRadians(90.0);
|
||||
export const RADIANS_180 = degreesToRadians(180.0);
|
||||
export const RADIANS_270 = degreesToRadians(270.0);
|
||||
|
||||
export function lerp(value: number, start: number, end: number) {
|
||||
return (1 - value) * start + value * end;
|
||||
}
|
||||
|
||||
export function nearlyEqual(a: number, b: number, tolerance: number = 0.0001) {
|
||||
return Math.abs(a - b) < tolerance;
|
||||
}
|
||||
|
||||
export function degreesToRadians(degrees: number) {
|
||||
return degrees * (Math.PI / 180.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a float in [0, 1] to an int in [0, 255]
|
||||
* @param decimal A number in [0, 1]
|
||||
*/
|
||||
export function uint8(decimal: number) {
|
||||
return Math.floor(decimal * 255);
|
||||
}
|
||||
|
||||
export function largestPowerOfTwoLessThanN(n: number) {
|
||||
return Math.floor(Math.log2(n));
|
||||
}
|
||||
}
|
||||
|
||||
export const argMax = (array: [number]) => {
|
||||
return array.map((x, i) => [x, i]).reduce((r, a) => (a[0] > r[0] ? a : r))[1];
|
||||
};
|
||||
|
||||
export const clamp = (value: number, min: number, max: number) => {
|
||||
return Math.max(Math.min(max, value), min);
|
||||
};
|
||||
|
||||
export const floorToNearest = (value: number, base: number) => {
|
||||
return Math.floor(value / base) * base;
|
||||
};
|
||||
|
||||
export const ceilToNearest = (value: number, base: number) => {
|
||||
return Math.ceil(value / base) * base;
|
||||
};
|
||||
|
||||
export const roundToNearest = (value: number, base: number) => {
|
||||
return Math.round(value / base) * base;
|
||||
};
|
||||
|
||||
export const between = (value: number, min: number, max: number) => {
|
||||
return min <= value && value <= max;
|
||||
};
|
||||
|
||||
export const mapRange = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin;
|
||||
};
|
||||
|
||||
export const wayThrough = (value: number, min: number, max: number) => {
|
||||
// ASSERT(value >= min && value <= max);
|
||||
return (value - min) / (max - min);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returs true if any number in args is NaN
|
||||
*/
|
||||
export const anyNaN = (...args: number[]) => {
|
||||
return args.some((arg) => {
|
||||
return isNaN(arg);
|
||||
});
|
||||
};
|
||||
|
||||
export const degreesToRadians = Math.PI / 180;
|
||||
|
||||
export class SmoothVariable {
|
||||
private _actual: number;
|
||||
private _target: number;
|
||||
private _smoothing: number;
|
||||
private _min: number;
|
||||
private _max: number;
|
||||
|
||||
public constructor(value: number, smoothing: number) {
|
||||
this._actual = value;
|
||||
this._target = value;
|
||||
this._smoothing = smoothing;
|
||||
this._min = -Infinity;
|
||||
this._max = Infinity;
|
||||
}
|
||||
|
||||
public setClamp(min: number, max: number) {
|
||||
this._min = min;
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
public addToTarget(delta: number) {
|
||||
this._target = clamp(this._target + delta, this._min, this._max);
|
||||
}
|
||||
|
||||
public setTarget(target: number) {
|
||||
this._target = clamp(target, this._min, this._max);
|
||||
}
|
||||
|
||||
public setActual(actual: number) {
|
||||
this._actual = actual;
|
||||
}
|
||||
|
||||
public tick() {
|
||||
this._actual += (this._target - this._actual) * this._smoothing;
|
||||
}
|
||||
|
||||
public getActual() {
|
||||
return this._actual;
|
||||
}
|
||||
|
||||
public getTarget() {
|
||||
return this._target;
|
||||
}
|
||||
}
|
||||
|
||||
export class SmoothVectorVariable {
|
||||
private _actual: Vector3;
|
||||
private _target: Vector3;
|
||||
private _smoothing: number;
|
||||
|
||||
public constructor(value: Vector3, smoothing: number) {
|
||||
this._actual = value;
|
||||
this._target = value;
|
||||
this._smoothing = smoothing;
|
||||
}
|
||||
|
||||
public addToTarget(delta: Vector3) {
|
||||
this._target = Vector3.add(this._target, delta);
|
||||
}
|
||||
|
||||
public setTarget(target: Vector3) {
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
public tick() {
|
||||
this._actual.add(Vector3.sub(this._target, this._actual).mulScalar(this._smoothing));
|
||||
}
|
||||
|
||||
public getActual() {
|
||||
return this._actual;
|
||||
}
|
||||
|
||||
public getTarget() {
|
||||
return this._target;
|
||||
}
|
||||
}
|
138
Core/src/ots_block_mesh.ts
Normal file
@ -0,0 +1,138 @@
|
||||
|
||||
import { FaceInfo } from "./block_atlas";
|
||||
import { Bounds } from "./bounds";
|
||||
import { Vector3 } from "./vector"
|
||||
|
||||
export type OtS_Block = {
|
||||
position: Vector3,
|
||||
name: string,
|
||||
}
|
||||
|
||||
type OtS_Block_Internal = OtS_Block & {
|
||||
}
|
||||
|
||||
export class OtS_BlockMesh {
|
||||
private _blocks: Map<number, OtS_Block_Internal>;
|
||||
private _isBoundsDirty: boolean;
|
||||
private _bounds: Bounds;
|
||||
|
||||
public constructor() {
|
||||
this._blocks = new Map();
|
||||
this._bounds = Bounds.getEmptyBounds();
|
||||
this._isBoundsDirty = false;
|
||||
}
|
||||
|
||||
public addBlock(x: number, y: number, z: number, blockName: string, replace: boolean) {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
let block: (OtS_Block_Internal | undefined) = this._blocks.get(key);
|
||||
|
||||
if (block === undefined) {
|
||||
const position = new Vector3(x, y, z);
|
||||
block = {
|
||||
position: position,
|
||||
name: blockName,
|
||||
}
|
||||
this._blocks.set(key, block);
|
||||
this._isBoundsDirty = true;
|
||||
} else if (replace) {
|
||||
block.name = blockName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a block from a given location.
|
||||
*/
|
||||
public removeBlock(x: number, y: number, z: number): boolean {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
const didRemove = this._blocks.delete(key);
|
||||
this._isBoundsDirty ||= didRemove;
|
||||
return didRemove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the colour of a voxel at a location, if one exists.
|
||||
* @note Modifying the returned colour will not update the voxel's colour.
|
||||
* For that, use `addVoxel` with the replaceMode set to 'replace'
|
||||
*/
|
||||
public getBlockAt(x: number, y: number, z: number): (OtS_Block | null) {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
const block = this._blocks.get(key);
|
||||
|
||||
if (block === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
position: block.position.copy(),
|
||||
name: block.name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not there is a voxel at a given location.
|
||||
*/
|
||||
public isBlockAt(x: number, y: number, z: number) {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
return this._blocks.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds/dimensions of the VoxelMesh.
|
||||
*/
|
||||
public getBounds(): Bounds {
|
||||
if (this._isBoundsDirty) {
|
||||
this._bounds = Bounds.getEmptyBounds();
|
||||
this._blocks.forEach((value, key) => {
|
||||
this._bounds.extendByPoint(value.position);
|
||||
});
|
||||
this._isBoundsDirty = false;
|
||||
}
|
||||
return this._bounds.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of voxels in the VoxelMesh.
|
||||
*/
|
||||
public getBlockCount(): number {
|
||||
return this._blocks.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the voxels in this VoxelMesh, note that these are copies
|
||||
* and editing each entry will not modify the underlying voxel.
|
||||
*/
|
||||
public getBlocks(): IterableIterator<OtS_Block> {
|
||||
const blocksCopy: OtS_Block[] = Array.from(this._blocks.values()).map((block) => {
|
||||
return {
|
||||
position: block.position.copy(),
|
||||
name: block.name,
|
||||
};
|
||||
});
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
return {
|
||||
[Symbol.iterator]: function () {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
if (currentIndex < blocksCopy.length) {
|
||||
const block = blocksCopy[currentIndex++];
|
||||
return { done: false, value: block };
|
||||
} else {
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public calcBlocksUsed(): Set<string> {
|
||||
const blocksUsed = new Set<string>();
|
||||
|
||||
for (const block of this.getBlocks()) {
|
||||
blocksUsed.add(block.name);
|
||||
}
|
||||
|
||||
return blocksUsed;
|
||||
}
|
||||
}
|
305
Core/src/ots_block_mesh_converter.ts
Normal file
@ -0,0 +1,305 @@
|
||||
import { OtS_VoxelMesh } from './ots_voxel_mesh';
|
||||
import { OtS_BlockMesh } from './ots_block_mesh';
|
||||
import { OtS_FaceVisibility, OtS_VoxelMesh_Neighbourhood } from './ots_voxel_mesh_neighbourhood';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
import { RGBA, OtS_Colours, RGBAUtil } from './colour';
|
||||
|
||||
export type OtS_BlockData_PerBlock<T> = { name: string, colour: T }[];
|
||||
|
||||
export type OtS_BlockTextureData_Block = {
|
||||
name: string,
|
||||
textures: {
|
||||
up: string,
|
||||
down: string,
|
||||
north: string,
|
||||
south: string,
|
||||
east: string,
|
||||
west: string,
|
||||
}
|
||||
}
|
||||
|
||||
export type OtS_BlockData_PerFace<T> = {
|
||||
blocks: OtS_BlockTextureData_Block[],
|
||||
textures: { [name: string]: T },
|
||||
}
|
||||
|
||||
type OtS_BlockMesh_DataMode<T> =
|
||||
| { type: 'per-block', data: OtS_BlockData_PerBlock<T> }
|
||||
| { type: 'per-face', data: OtS_BlockData_PerFace<T> }
|
||||
|
||||
export type OtS_FallableBehaviour = 'replace-falling' | 'replace-fallable' | 'place-string';
|
||||
|
||||
export type OtS_BlockMesh_ConverterConfig = {
|
||||
mode: OtS_BlockMesh_DataMode<RGBA>,
|
||||
dithering?: { mode: 'random' | 'ordered', magnitude: number },
|
||||
fallable?: OtS_FallableBehaviour,
|
||||
smoothness?: OtS_BlockMesh_DataMode<number> & { weight: number },
|
||||
resolution?: number, // [1, 255]
|
||||
}
|
||||
|
||||
export class OtS_BlockMesh_Converter {
|
||||
private _config: OtS_BlockMesh_ConverterConfig;
|
||||
|
||||
public constructor() {
|
||||
this._config = {
|
||||
mode: {
|
||||
type: 'per-block', data: [
|
||||
{ name: 'minecraft:stone', colour: OtS_Colours.WHITE }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set the config.
|
||||
* Returns false if the supplied config is invalid.
|
||||
*/
|
||||
public setConfig(config: OtS_BlockMesh_ConverterConfig): boolean {
|
||||
// TODO: Validate
|
||||
|
||||
// TODO: Validata per-face data has colours for each
|
||||
|
||||
// TODO: Copy config
|
||||
|
||||
this._config = config;
|
||||
return true;
|
||||
}
|
||||
|
||||
public process(voxelMesh: OtS_VoxelMesh): OtS_BlockMesh {
|
||||
const blockMesh = new OtS_BlockMesh();
|
||||
|
||||
// TODO: Fallable
|
||||
// TODO: Smoothness
|
||||
// TODO: Dithering
|
||||
|
||||
let neighbourhood: OtS_VoxelMesh_Neighbourhood | undefined;
|
||||
if (this._config.mode.type === 'per-face') {
|
||||
neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'cardinal');
|
||||
}
|
||||
|
||||
let caches: Array<Map<number, string>>;
|
||||
if (this._config.mode.type === 'per-face') {
|
||||
caches = new Array<Map<number, string>>(64);
|
||||
for (let i = 0; i < caches.length; ++i) {
|
||||
caches[i] = new Map();
|
||||
}
|
||||
} else {
|
||||
caches = new Array(1);
|
||||
caches[0] = new Map();
|
||||
}
|
||||
|
||||
for (const { position, colour } of voxelMesh.getVoxels()) {
|
||||
const binnedColour = RGBAUtil.bin(colour, this._config.resolution ?? 255);
|
||||
const binnedHash = RGBAUtil.hash255(binnedColour);
|
||||
|
||||
let cache = caches[0];
|
||||
if (neighbourhood) {
|
||||
const visibility = neighbourhood.getFaceVisibility(position.x, position.y, position.z);
|
||||
ASSERT(visibility !== null);
|
||||
cache = caches[visibility];
|
||||
}
|
||||
const cachedBlock = cache.get(binnedHash);
|
||||
if (cachedBlock !== undefined) {
|
||||
blockMesh.addBlock(position.x, position.y, position.z, cachedBlock, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
let block: (string | null) = null;
|
||||
if (this._config.mode.type === 'per-block') {
|
||||
block = this._findClosestBlock_PerBlock(colour);
|
||||
} else {
|
||||
block = this._findClosestBlock_PerFace(colour, position, neighbourhood!);
|
||||
}
|
||||
|
||||
if (block === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cache.set(binnedHash, block);
|
||||
|
||||
blockMesh.addBlock(position.x, position.y, position.z, block, true);
|
||||
}
|
||||
|
||||
return blockMesh;
|
||||
}
|
||||
|
||||
private _getBlockNames(): string[] {
|
||||
if (this._config.mode.type === 'per-block') {
|
||||
return Object.keys(this._config.mode.data);
|
||||
} else {
|
||||
return this._config.mode.data.blocks.map((block) => block.name );
|
||||
}
|
||||
}
|
||||
|
||||
private _findClosestBlock_PerBlock(desiredColour: RGBA) {
|
||||
ASSERT(this._config.mode.type === 'per-block');
|
||||
|
||||
let bestDistance = Infinity;
|
||||
let bestCandidate: (string | null) = null;
|
||||
|
||||
for (const { name, colour } of this._config.mode.data) {
|
||||
const distance = RGBAUtil.squaredDistance(colour, desiredColour);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestCandidate = name;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
|
||||
private _findClosestBlock_PerFace(desiredColour: RGBA, position: Vector3, neighbourhood: OtS_VoxelMesh_Neighbourhood) {
|
||||
ASSERT(this._config.mode.type === 'per-face');
|
||||
|
||||
let bestDistance = Infinity;
|
||||
let bestCandidate: (string | null) = null;
|
||||
|
||||
const visibility = neighbourhood.getFaceVisibility(position.x, position.y, position.z);
|
||||
if (visibility === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const block of this._config.mode.data.blocks) {
|
||||
|
||||
|
||||
const averageColour: RGBA = { r: 0, g: 0, b: 0, a: 0 };
|
||||
{
|
||||
let count = 0;
|
||||
|
||||
if (visibility & OtS_FaceVisibility.Up) {
|
||||
const faceTexture = block.textures.up;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.Down) {
|
||||
const faceTexture = block.textures.down;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.North) {
|
||||
const faceTexture = block.textures.north;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.East) {
|
||||
const faceTexture = block.textures.east;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.South) {
|
||||
const faceTexture = block.textures.south;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.West) {
|
||||
const faceTexture = block.textures.west;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
|
||||
averageColour.r /= count;
|
||||
averageColour.g /= count;
|
||||
averageColour.b /= count;
|
||||
averageColour.a /= count;
|
||||
}
|
||||
|
||||
const distance = RGBAUtil.squaredDistance(averageColour, desiredColour);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
bestCandidate = block.name;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCandidate;
|
||||
}
|
||||
|
||||
/*
|
||||
private _getBlocks(visibility: OtS_FaceVisibility): IterableIterator<{ name: string, colour: RGBA }> {
|
||||
let currentIndex = 0;
|
||||
|
||||
const blockCount = this._config.mode.type === 'per-block'
|
||||
? this._config.mode.data.length
|
||||
: this._config.mode.data.blocks.length;
|
||||
|
||||
const getBlockColour = (index: number): { name: string, colour: RGBA } => {
|
||||
if (this._config.mode.type === 'per-block') {
|
||||
return this._config.mode.data[index];
|
||||
} else {
|
||||
const block = this._config.mode.data.blocks[index];
|
||||
|
||||
const averageColour: RGBA = { r: 0, g: 0, b: 0, a: 0 };
|
||||
let count = 0;
|
||||
if (visibility & OtS_FaceVisibility.Up) {
|
||||
const faceTexture = block.textures.up;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.Down) {
|
||||
const faceTexture = block.textures.down;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.North) {
|
||||
const faceTexture = block.textures.north;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.East) {
|
||||
const faceTexture = block.textures.east;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.South) {
|
||||
const faceTexture = block.textures.south;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
if (visibility & OtS_FaceVisibility.West) {
|
||||
const faceTexture = block.textures.west;
|
||||
const faceColour = this._config.mode.data.textures[faceTexture];
|
||||
RGBAUtil.add(averageColour, faceColour);
|
||||
++count;
|
||||
}
|
||||
averageColour.r /= count;
|
||||
averageColour.g /= count;
|
||||
averageColour.b /= count;
|
||||
averageColour.a /= count;
|
||||
|
||||
return {
|
||||
name: block.name,
|
||||
colour: averageColour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[Symbol.iterator]: function () {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
if (currentIndex < blockCount) {
|
||||
const block = getBlockColour(currentIndex);
|
||||
++currentIndex;
|
||||
|
||||
return { done: false, value: block };
|
||||
} else {
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
*/
|
||||
}
|
75
Core/src/ots_materials.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { RGBA, RGBAUtil } from "./colour";
|
||||
//import { Material, OtS_Util } from "./materials";
|
||||
import { OtS_Texture } from "./ots_texture";
|
||||
|
||||
export type OtS_MeshSection = { name: string, positionData: Float32Array, normalData: Float32Array, indexData: Uint32Array } & (
|
||||
| { type: 'solid', colour: RGBA }
|
||||
| { type: 'colour', colourData: Float32Array }
|
||||
| { type: 'textured', texcoordData: Float32Array, texture: OtS_Texture });
|
||||
|
||||
export namespace OtS_MeshUtil {
|
||||
export function copySection(section: OtS_MeshSection): OtS_MeshSection {
|
||||
switch(section.type) {
|
||||
case 'solid':
|
||||
return {
|
||||
type: 'solid',
|
||||
name: section.name,
|
||||
indexData: section.indexData.slice(0),
|
||||
positionData: section.positionData.slice(0),
|
||||
normalData: section.normalData.slice(0),
|
||||
colour: RGBAUtil.copy(section.colour),
|
||||
};
|
||||
case 'colour':
|
||||
return {
|
||||
type: 'colour',
|
||||
name: section.name,
|
||||
indexData: section.indexData.slice(0),
|
||||
positionData: section.positionData.slice(0),
|
||||
normalData: section.normalData.slice(0),
|
||||
colourData: section.colourData.slice(0),
|
||||
};
|
||||
case 'textured':
|
||||
return {
|
||||
type: 'textured',
|
||||
name: section.name,
|
||||
indexData: section.indexData.slice(0),
|
||||
positionData: section.positionData.slice(0),
|
||||
normalData: section.normalData.slice(0),
|
||||
texcoordData: section.texcoordData.slice(0),
|
||||
texture: section.texture.copy(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export class OtS_MaterialSlots {
|
||||
private _slots: Map<number, Material>;
|
||||
|
||||
private constructor() {
|
||||
this._slots = new Map();
|
||||
}
|
||||
|
||||
public static create() {
|
||||
return new OtS_MaterialSlots();
|
||||
}
|
||||
|
||||
public setSlot(index: number, material: Material) {
|
||||
this._slots.set(index, material);
|
||||
}
|
||||
|
||||
public getSlot(index: number) {
|
||||
return this._slots.get(index);
|
||||
}
|
||||
|
||||
public copy() {
|
||||
const clone = OtS_MaterialSlots.create();
|
||||
|
||||
this._slots.forEach((value, key) => {
|
||||
clone.setSlot(key, OtS_Util.copyMaterial(value));
|
||||
});
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
*/
|
432
Core/src/ots_mesh.ts
Normal file
@ -0,0 +1,432 @@
|
||||
import { Bounds } from './bounds';
|
||||
import { RGBA } from './colour';
|
||||
import { degreesToRadians } from './math';
|
||||
import { OtS_MeshSection, OtS_MeshUtil } from './ots_materials';
|
||||
import { OtS_Texture } from './ots_texture';
|
||||
import { UV } from "./util";
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Result } from './util/type_util';
|
||||
import { Vector3 } from "./vector";
|
||||
|
||||
type OtS_VertexData<T> = {
|
||||
v0: T,
|
||||
v1: T,
|
||||
v2: T,
|
||||
}
|
||||
|
||||
export type OtS_Triangle =
|
||||
| { type: 'solid', colour: RGBA, data: OtS_VertexData<{ position: Vector3, normal: Vector3 }> }
|
||||
| { type: 'coloured', data: OtS_VertexData<{ position: Vector3, normal: Vector3, colour: RGBA }> }
|
||||
| { type: 'textured', texture: OtS_Texture, data: OtS_VertexData<{ position: Vector3, normal: Vector3, texcoord: UV }> }
|
||||
|
||||
type OtS_MeshError = 'bad-height' | 'bad-material-match' | 'bad-geometry';
|
||||
|
||||
export type OtS_MeshSectionMetadata = { name: string } & (
|
||||
| { type: 'solid', colour: RGBA }
|
||||
| { type: 'colour' }
|
||||
| { type: 'textured', texture: OtS_Texture }
|
||||
);
|
||||
|
||||
export class OtS_Mesh {
|
||||
private _sections: OtS_MeshSection[];
|
||||
|
||||
private constructor() {
|
||||
this._sections = [];
|
||||
}
|
||||
|
||||
public static create() {
|
||||
return new this();
|
||||
/*
|
||||
// TODO: Check non-zero height
|
||||
if (false) {
|
||||
return { ok: false, error: {
|
||||
code: 'bad-height',
|
||||
message: 'Geometry should have a non-zero height, consider rotating the mesh'
|
||||
}};
|
||||
}
|
||||
|
||||
// TODO: Check geometry using materials slots is valid, i.e. a triangle that uses a
|
||||
// textured material must have texcoords
|
||||
if (false) {
|
||||
return { ok: false, error: {
|
||||
code: 'bad-material-match',
|
||||
message: 'Material \'x\' uses a textured material but has no texcoords'
|
||||
}};
|
||||
}
|
||||
|
||||
// TODO: Check material slots used by geometry are defined
|
||||
|
||||
if (!geometry.hasAttribute('position')) {
|
||||
return { ok: false, error: {
|
||||
code: 'bad-geometry',
|
||||
message: 'Missing position data'
|
||||
}};
|
||||
}
|
||||
|
||||
const mesh = new this(geometry, materials);
|
||||
return { ok: true, value: mesh };
|
||||
*/
|
||||
}
|
||||
|
||||
public addSection(section: OtS_MeshSection): Result<void, OtS_MeshError> {
|
||||
// TODO: Validation
|
||||
|
||||
// TODO: Ensure section names do not clash
|
||||
|
||||
this._sections.push(section);
|
||||
return { ok: true, value: undefined };
|
||||
}
|
||||
|
||||
public translate(x: number, y: number, z: number) {
|
||||
this._sections.forEach((section) => {
|
||||
for (let i = 0; i < section.positionData.length; i += 3) {
|
||||
section.positionData[i + 0] += x;
|
||||
section.positionData[i + 1] += y;
|
||||
section.positionData[i + 2] += z;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public scale(s: number) {
|
||||
this._sections.forEach((section) => {
|
||||
for (let i = 0; i < section.positionData.length; i += 3) {
|
||||
section.positionData[i + 0] *= s;
|
||||
section.positionData[i + 1] *= s;
|
||||
section.positionData[i + 2] *= s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public centre() {
|
||||
const centre = this.calcBounds().getCentre();
|
||||
this.translate(-centre.x, -centre.y, -centre.z);
|
||||
}
|
||||
|
||||
public normalise(): boolean {
|
||||
const bounds = this.calcBounds();
|
||||
const size = Vector3.sub(bounds.max, bounds.min);
|
||||
const scaleFactor = 1.0 / size.y;
|
||||
|
||||
if (isNaN(scaleFactor) || !isFinite(scaleFactor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.scale(scaleFactor);
|
||||
return true;
|
||||
}
|
||||
|
||||
public rotate(pitch: number, roll: number, yaw: number) {
|
||||
const cosa = Math.cos(yaw * degreesToRadians);
|
||||
const sina = Math.sin(yaw * degreesToRadians);
|
||||
|
||||
const cosb = Math.cos(pitch * degreesToRadians);
|
||||
const sinb = Math.sin(pitch * degreesToRadians);
|
||||
|
||||
const cosc = Math.cos(roll * degreesToRadians);
|
||||
const sinc = Math.sin(roll * degreesToRadians);
|
||||
|
||||
const Axx = cosa*cosb;
|
||||
const Axy = cosa*sinb*sinc - sina*cosc;
|
||||
const Axz = cosa*sinb*cosc + sina*sinc;
|
||||
|
||||
const Ayx = sina*cosb;
|
||||
const Ayy = sina*sinb*sinc + cosa*cosc;
|
||||
const Ayz = sina*sinb*cosc - cosa*sinc;
|
||||
|
||||
const Azx = -sinb;
|
||||
const Azy = cosb*sinc;
|
||||
const Azz = cosb*cosc;
|
||||
|
||||
this._sections.forEach((section) => {
|
||||
for (let i = 0; i < section.positionData.length; i += 3) {
|
||||
const px = section.positionData[i + 0];
|
||||
const py = section.positionData[i + 1];
|
||||
const pz = section.positionData[i + 2];
|
||||
|
||||
section.positionData[i + 0] = Axx * px + Axy * py + Axz * pz;
|
||||
section.positionData[i + 1] = Ayx * px + Ayy * py + Ayz * pz;
|
||||
section.positionData[i + 2] = Azx * px + Azy * py + Azz * pz;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @note Returns a reference to the underlying materials, modifying these is dangerous
|
||||
*/
|
||||
public getTriangles(): IterableIterator<OtS_Triangle> {
|
||||
const sectionCount = this._sections.length;
|
||||
ASSERT(sectionCount > 0); // TODO: Don't assert,
|
||||
|
||||
let sectionIndex = 0;
|
||||
let triangleCount = this._sections[0].indexData.length / 3;
|
||||
let triangleIndex = 0;
|
||||
|
||||
return {
|
||||
[Symbol.iterator]: function () {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
if (triangleIndex >= triangleCount && sectionIndex < sectionCount - 1) {
|
||||
++sectionIndex;
|
||||
triangleCount = this._sections[sectionIndex].indexData.length / 3;
|
||||
triangleIndex = 0;
|
||||
}
|
||||
|
||||
if (triangleIndex < triangleCount) {
|
||||
const section = this._sections[sectionIndex];
|
||||
|
||||
const index0 = section.indexData[triangleIndex * 3 + 0];
|
||||
const index1 = section.indexData[triangleIndex * 3 + 1];
|
||||
const index2 = section.indexData[triangleIndex * 3 + 2];
|
||||
|
||||
++triangleIndex;
|
||||
|
||||
switch (section.type) {
|
||||
case 'solid': {
|
||||
const triangle: OtS_Triangle = {
|
||||
type: 'solid',
|
||||
colour: section.colour,
|
||||
data: {
|
||||
v0: {
|
||||
position: new Vector3(
|
||||
section.positionData[index0 * 3 + 0],
|
||||
section.positionData[index0 * 3 + 1],
|
||||
section.positionData[index0 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index0 * 3 + 0],
|
||||
section.normalData[index0 * 3 + 1],
|
||||
section.normalData[index0 * 3 + 2],
|
||||
),
|
||||
},
|
||||
v1: {
|
||||
position: new Vector3(
|
||||
section.positionData[index1 * 3 + 0],
|
||||
section.positionData[index1 * 3 + 1],
|
||||
section.positionData[index1 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index1 * 3 + 0],
|
||||
section.normalData[index1 * 3 + 1],
|
||||
section.normalData[index1 * 3 + 2],
|
||||
),
|
||||
},
|
||||
v2: {
|
||||
position: new Vector3(
|
||||
section.positionData[index2 * 3 + 0],
|
||||
section.positionData[index2 * 3 + 1],
|
||||
section.positionData[index2 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index2 * 3 + 0],
|
||||
section.normalData[index2 * 3 + 1],
|
||||
section.normalData[index2 * 3 + 2],
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
return { done: false, value: triangle };
|
||||
}
|
||||
case 'colour': {
|
||||
const triangle: OtS_Triangle = {
|
||||
type: 'coloured',
|
||||
data: {
|
||||
v0: {
|
||||
position: new Vector3(
|
||||
section.positionData[index0 * 3 + 0],
|
||||
section.positionData[index0 * 3 + 1],
|
||||
section.positionData[index0 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index0 * 3 + 0],
|
||||
section.normalData[index0 * 3 + 1],
|
||||
section.normalData[index0 * 3 + 2],
|
||||
),
|
||||
colour: {
|
||||
r: section.colourData[index0 * 4 + 0],
|
||||
g: section.colourData[index0 * 4 + 1],
|
||||
b: section.colourData[index0 * 4 + 2],
|
||||
a: section.colourData[index0 * 4 + 3],
|
||||
},
|
||||
},
|
||||
v1: {
|
||||
position: new Vector3(
|
||||
section.positionData[index1 * 3 + 0],
|
||||
section.positionData[index1 * 3 + 1],
|
||||
section.positionData[index1 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index1 * 3 + 0],
|
||||
section.normalData[index1 * 3 + 1],
|
||||
section.normalData[index1 * 3 + 2],
|
||||
),
|
||||
colour: {
|
||||
r: section.colourData[index1 * 4 + 0],
|
||||
g: section.colourData[index1 * 4 + 1],
|
||||
b: section.colourData[index1 * 4 + 2],
|
||||
a: section.colourData[index1 * 4 + 3],
|
||||
},
|
||||
},
|
||||
v2: {
|
||||
position: new Vector3(
|
||||
section.positionData[index2 * 3 + 0],
|
||||
section.positionData[index2 * 3 + 1],
|
||||
section.positionData[index2 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index2 * 3 + 0],
|
||||
section.normalData[index2 * 3 + 1],
|
||||
section.normalData[index2 * 3 + 2],
|
||||
),
|
||||
colour: {
|
||||
r: section.colourData[index2 * 4 + 0],
|
||||
g: section.colourData[index2 * 4 + 1],
|
||||
b: section.colourData[index2 * 4 + 2],
|
||||
a: section.colourData[index2 * 4 + 3],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return { done: false, value: triangle };
|
||||
}
|
||||
case 'textured': {
|
||||
const triangle: OtS_Triangle = {
|
||||
type: 'textured',
|
||||
texture: section.texture,
|
||||
data: {
|
||||
v0: {
|
||||
position: new Vector3(
|
||||
section.positionData[index0 * 3 + 0],
|
||||
section.positionData[index0 * 3 + 1],
|
||||
section.positionData[index0 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index0 * 3 + 0],
|
||||
section.normalData[index0 * 3 + 1],
|
||||
section.normalData[index0 * 3 + 2],
|
||||
),
|
||||
texcoord: {
|
||||
u: section.texcoordData[index0 * 2 + 0],
|
||||
v: section.texcoordData[index0 * 2 + 1],
|
||||
},
|
||||
},
|
||||
v1: {
|
||||
position: new Vector3(
|
||||
section.positionData[index1 * 3 + 0],
|
||||
section.positionData[index1 * 3 + 1],
|
||||
section.positionData[index1 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index1 * 3 + 0],
|
||||
section.normalData[index1 * 3 + 1],
|
||||
section.normalData[index1 * 3 + 2],
|
||||
),
|
||||
texcoord: {
|
||||
u: section.texcoordData[index1 * 2 + 0],
|
||||
v: section.texcoordData[index1 * 2 + 1],
|
||||
},
|
||||
},
|
||||
v2: {
|
||||
position: new Vector3(
|
||||
section.positionData[index2 * 3 + 0],
|
||||
section.positionData[index2 * 3 + 1],
|
||||
section.positionData[index2 * 3 + 2],
|
||||
),
|
||||
normal: new Vector3(
|
||||
section.normalData[index2 * 3 + 0],
|
||||
section.normalData[index2 * 3 + 1],
|
||||
section.normalData[index2 * 3 + 2],
|
||||
),
|
||||
texcoord: {
|
||||
u: section.texcoordData[index2 * 2 + 0],
|
||||
v: section.texcoordData[index2 * 2 + 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return { done: false, value: triangle };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { done: true, value: undefined };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public copy() {
|
||||
const clone = OtS_Mesh.create();
|
||||
|
||||
this.getSectionData().forEach((section) => {
|
||||
const success = clone.addSection(section).ok;
|
||||
ASSERT(success);
|
||||
})
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public calcBounds() {
|
||||
const bounds = Bounds.getEmptyBounds();
|
||||
|
||||
const vec = new Vector3(0, 0, 0);
|
||||
this._sections.forEach((section) => {
|
||||
for (let i = 0; i < section.positionData.length; i += 3) {
|
||||
vec.set(
|
||||
section.positionData[i + 0],
|
||||
section.positionData[i + 1],
|
||||
section.positionData[i + 2],
|
||||
);
|
||||
bounds.extendByPoint(vec);
|
||||
}
|
||||
});
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public calcTriangleCount() {
|
||||
return this._sections
|
||||
.map((section) => {
|
||||
return section.indexData.length / 3;
|
||||
})
|
||||
.reduce((total, count) => total + count, 0);
|
||||
}
|
||||
|
||||
public getSectionData(): OtS_MeshSection[] {
|
||||
return this._sections.map((section) => {
|
||||
return OtS_MeshUtil.copySection(section);
|
||||
})
|
||||
}
|
||||
|
||||
public getSectionMetadata(): OtS_MeshSectionMetadata[] {
|
||||
const metadata: OtS_MeshSectionMetadata[] = [];
|
||||
|
||||
this._sections.forEach((section) => {
|
||||
let entry: OtS_MeshSectionMetadata;
|
||||
switch (section.type) {
|
||||
case 'solid':
|
||||
entry = {
|
||||
type: 'solid',
|
||||
name: section.name,
|
||||
colour: section.colour,
|
||||
};
|
||||
break;
|
||||
case 'colour': {
|
||||
entry = {
|
||||
type: 'colour',
|
||||
name: section.name,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'textured':
|
||||
entry = {
|
||||
type: 'textured',
|
||||
name: section.name,
|
||||
texture: section.texture,
|
||||
}
|
||||
}
|
||||
metadata.push(entry);
|
||||
});
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
138
Core/src/ots_texture.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { RGBA, RGBAUtil } from "./colour";
|
||||
import { clamp } from "./math";
|
||||
import { TTexelExtension, TTexelInterpolation } from "./util/type_util";
|
||||
|
||||
export class OtS_Texture {
|
||||
private _data: Uint8ClampedArray;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
private _interpolation: TTexelInterpolation;
|
||||
private _extension: TTexelExtension;
|
||||
|
||||
/**
|
||||
* Expects `data` to contain RGBA data, i.e. `width*height*4` elements.
|
||||
*/
|
||||
public constructor(data: Uint8ClampedArray, width: number, height: number, interpolation: TTexelInterpolation, extension: TTexelExtension) {
|
||||
this._data = data;
|
||||
this._width = width;
|
||||
this._height = height;
|
||||
this._interpolation = interpolation;
|
||||
this._extension = extension;
|
||||
}
|
||||
|
||||
public sample(u: number, v: number): RGBA {
|
||||
const sampleU = this._extension === 'clamp'
|
||||
? this._clampValue(u)
|
||||
: this._repeatValue(u);
|
||||
|
||||
const sampleV = this._extension === 'clamp'
|
||||
? 1.0 - this._clampValue(v)
|
||||
: 1.0 - this._repeatValue(v);
|
||||
|
||||
return this._interpolation === 'nearest'
|
||||
? this._sampleNearest(sampleU, sampleV)
|
||||
: this._sampleLinear(sampleU, sampleV);
|
||||
}
|
||||
|
||||
public getWidth() {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public getHeight() {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public getInterpolation() {
|
||||
return this._interpolation;
|
||||
}
|
||||
|
||||
public getExtension() {
|
||||
return this._extension;
|
||||
}
|
||||
|
||||
public getData() {
|
||||
return this._data.slice(0);
|
||||
}
|
||||
|
||||
public copy() {
|
||||
return new OtS_Texture(
|
||||
this._data.slice(0),
|
||||
this._width,
|
||||
this._height,
|
||||
this._interpolation,
|
||||
this._extension,
|
||||
);
|
||||
}
|
||||
|
||||
// Assumes `u` and `v` are in the range [0, 1]
|
||||
private _sampleLinear(u: number, v: number): RGBA {
|
||||
const x = Math.floor(u * (this._width - 1));
|
||||
const y = Math.floor(v * (this._height - 1));
|
||||
|
||||
const left = Math.floor(x);
|
||||
const right = left + 1;
|
||||
|
||||
const top = Math.floor(y);
|
||||
const bottom = top + 1;
|
||||
|
||||
const AB = RGBAUtil.lerp(
|
||||
this._samplePixel(left, top),
|
||||
this._samplePixel(right, top),
|
||||
x - left,
|
||||
);
|
||||
|
||||
const CD = RGBAUtil.lerp(
|
||||
this._samplePixel(left, bottom),
|
||||
this._samplePixel(right, bottom),
|
||||
x - left,
|
||||
);
|
||||
|
||||
return RGBAUtil.lerp(AB, CD, y - top);
|
||||
}
|
||||
|
||||
// Assumes `u` and `v` are in the range [0, 1]
|
||||
private _sampleNearest(u: number, v: number): RGBA {
|
||||
const x = Math.round(u * (this._width - 1));
|
||||
const y = Math.round(v * (this._height - 1));
|
||||
|
||||
return this._samplePixel(x, y);
|
||||
}
|
||||
|
||||
// Expects `x` and `y` to be integers
|
||||
private _samplePixel(x: number, y: number): RGBA {
|
||||
const cx = clamp(x, 0, this._width - 1);
|
||||
const cy = clamp(y, 0, this._height - 1);
|
||||
|
||||
const index = 4 * (this._width * cy + cx);
|
||||
|
||||
return {
|
||||
r: this._data[index + 0] / 255,
|
||||
g: this._data[index + 1] / 255,
|
||||
b: this._data[index + 2] / 255,
|
||||
a: this._data[index + 3] / 255,
|
||||
};
|
||||
}
|
||||
|
||||
private _clampValue(x: number) {
|
||||
return clamp(x, 0.0, 1.0);
|
||||
}
|
||||
|
||||
private _repeatValue(x: number) {
|
||||
if (Number.isInteger(x)) {
|
||||
return x > 0.5 ? 1.0 : 0.0;
|
||||
}
|
||||
const frac = Math.abs(x) - Math.floor(Math.abs(x));
|
||||
return x < 0.0 ? 1.0 - frac : frac;
|
||||
}
|
||||
|
||||
public static CreateDebugTexture() {
|
||||
const data = Uint8ClampedArray.from([
|
||||
0, 0, 0, 255,
|
||||
255, 0, 255, 255,
|
||||
255, 0, 255, 255,
|
||||
0, 0, 0, 255,
|
||||
]);
|
||||
|
||||
return new OtS_Texture(data, 2, 2, 'nearest', 'repeat');
|
||||
}
|
||||
}
|
158
Core/src/ots_voxel_mesh.ts
Normal file
@ -0,0 +1,158 @@
|
||||
|
||||
import { Bounds } from "./bounds";
|
||||
import { RGBA, RGBAUtil } from './colour';
|
||||
import { Vector3 } from "./vector"
|
||||
|
||||
export type OtS_Voxel = {
|
||||
position: Vector3,
|
||||
colour: RGBA,
|
||||
}
|
||||
|
||||
export type OtS_ReplaceMode = 'replace' | 'keep' | 'average';
|
||||
|
||||
type OtS_Voxel_Internal = OtS_Voxel & {
|
||||
collisions: number,
|
||||
}
|
||||
|
||||
export class OtS_VoxelMesh {
|
||||
private _voxels: Map<number, OtS_Voxel_Internal>;
|
||||
private _isBoundsDirty: boolean;
|
||||
private _bounds: Bounds;
|
||||
private _replaceMode: OtS_ReplaceMode;
|
||||
|
||||
public constructor() {
|
||||
this._voxels = new Map();
|
||||
this._bounds = Bounds.getEmptyBounds();
|
||||
this._isBoundsDirty = false;
|
||||
this._replaceMode = 'average';
|
||||
}
|
||||
|
||||
public setReplaceMode(replaceMode: OtS_ReplaceMode) {
|
||||
this._replaceMode = replaceMode;
|
||||
}
|
||||
|
||||
public addVoxel(x: number, y: number, z: number, colour: RGBA, replaceMode?: OtS_ReplaceMode) {
|
||||
const useReplaceMode = replaceMode ?? this._replaceMode;
|
||||
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
let voxel: (OtS_Voxel_Internal | undefined) = this._voxels.get(key);
|
||||
|
||||
if (voxel === undefined) {
|
||||
const position = new Vector3(x, y, z);
|
||||
voxel = {
|
||||
position: position,
|
||||
colour: RGBAUtil.copy(colour),
|
||||
collisions: 1,
|
||||
}
|
||||
this._voxels.set(key, voxel);
|
||||
//this._bounds.extendByPoint(position);
|
||||
this._isBoundsDirty = true;
|
||||
} else {
|
||||
if (useReplaceMode === 'average') {
|
||||
voxel.colour.r = ((voxel.colour.r * voxel.collisions) + colour.r) / (voxel.collisions + 1);
|
||||
voxel.colour.g = ((voxel.colour.g * voxel.collisions) + colour.g) / (voxel.collisions + 1);
|
||||
voxel.colour.b = ((voxel.colour.b * voxel.collisions) + colour.b) / (voxel.collisions + 1);
|
||||
voxel.colour.a = ((voxel.colour.a * voxel.collisions) + colour.a) / (voxel.collisions + 1);
|
||||
++voxel.collisions;
|
||||
} else if (useReplaceMode === 'replace') {
|
||||
voxel.colour = RGBAUtil.copy(colour);
|
||||
voxel.collisions = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a voxel from a given location.
|
||||
*/
|
||||
public removeVoxel(x: number, y: number, z: number): boolean {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
const didRemove = this._voxels.delete(key);
|
||||
this._isBoundsDirty ||= didRemove;
|
||||
return didRemove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the colour of a voxel at a location, if one exists.
|
||||
* @note Modifying the returned colour will not update the voxel's colour.
|
||||
* For that, use `addVoxel` with the replaceMode set to 'replace'
|
||||
*/
|
||||
public getVoxelAt(x: number, y: number, z: number): (OtS_Voxel | null) {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
const voxel = this._voxels.get(key);
|
||||
|
||||
if (voxel === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
position: voxel.position.copy(),
|
||||
colour: RGBAUtil.copy(voxel.colour),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not there is a voxel at a given location.
|
||||
*/
|
||||
public isVoxelAt(x: number, y: number, z: number) {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
return this._voxels.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not there is a opaque voxel at a given location.
|
||||
*/
|
||||
public isOpaqueVoxelAt(x: number, y: number, z: number) {
|
||||
const voxel = this.getVoxelAt(x, y, z);
|
||||
return voxel === null ? false : voxel.colour.a === 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds/dimensions of the VoxelMesh.
|
||||
*/
|
||||
public getBounds(): Bounds {
|
||||
if (this._isBoundsDirty) {
|
||||
this._bounds = Bounds.getEmptyBounds();
|
||||
this._voxels.forEach((value, key) => {
|
||||
this._bounds.extendByPoint(value.position);
|
||||
});
|
||||
this._isBoundsDirty = false;
|
||||
}
|
||||
return this._bounds.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of voxels in the VoxelMesh.
|
||||
*/
|
||||
public getVoxelCount(): number {
|
||||
return this._voxels.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the voxels in this VoxelMesh, note that these are copies
|
||||
* and editing each entry will not modify the underlying voxel.
|
||||
*/
|
||||
public getVoxels(): IterableIterator<OtS_Voxel> {
|
||||
const voxelsCopy: OtS_Voxel[] = Array.from(this._voxels.values()).map((voxel) => {
|
||||
return {
|
||||
position: voxel.position.copy(),
|
||||
colour: RGBAUtil.copy(voxel.colour),
|
||||
}
|
||||
});
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
return {
|
||||
[Symbol.iterator]: function () {
|
||||
return this;
|
||||
},
|
||||
next: () => {
|
||||
if (currentIndex < voxelsCopy.length) {
|
||||
const voxel = voxelsCopy[currentIndex++];
|
||||
return { done: false, value: voxel };
|
||||
} else {
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
181
Core/src/ots_voxel_mesh_converter.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { OtS_ReplaceMode, OtS_VoxelMesh } from './ots_voxel_mesh';
|
||||
import { TAxis } from './util/type_util';
|
||||
import { Vector3 } from './vector';
|
||||
import { Triangle } from './triangle';
|
||||
import { OtS_Colours, RGBA, RGBAUtil } from './colour';
|
||||
import { OtS_Mesh, OtS_Triangle } from './ots_mesh';
|
||||
import { UV } from './util';
|
||||
import { rayIntersectTriangleFastX, rayIntersectTriangleFastY, rayIntersectTriangleFastZ, RayIntersect } from './ray';
|
||||
import { findFirstTrueIndex } from './util/array_util';
|
||||
|
||||
export type OtS_VoxelMesh_ConverterConfig = {
|
||||
constraintAxis: TAxis,
|
||||
size: number,
|
||||
multisampling?: number,
|
||||
replaceMode: OtS_ReplaceMode,
|
||||
}
|
||||
|
||||
export class OtS_VoxelMesh_Converter {
|
||||
private _config: OtS_VoxelMesh_ConverterConfig;
|
||||
|
||||
public constructor() {
|
||||
this._config = {
|
||||
constraintAxis: 'y',
|
||||
size: 80,
|
||||
multisampling: 8,
|
||||
replaceMode: 'average',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set the config.
|
||||
* Returns false if the supplied config is invalid.
|
||||
*/
|
||||
public setConfig(config: OtS_VoxelMesh_ConverterConfig): boolean {
|
||||
if (config.size <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
return true;
|
||||
}
|
||||
|
||||
public process(mesh: OtS_Mesh): OtS_VoxelMesh {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
|
||||
const { scale, offset } = this._calcScaleOffset(mesh);
|
||||
|
||||
const normalisedMesh = mesh.copy();
|
||||
normalisedMesh.scale(scale);
|
||||
normalisedMesh.translate(offset.x, offset.y, offset.z);
|
||||
|
||||
for (const triangle of normalisedMesh.getTriangles()) {
|
||||
this._handleTriangle(triangle, voxelMesh);
|
||||
}
|
||||
|
||||
return voxelMesh;
|
||||
}
|
||||
|
||||
private _handleTriangle(triangle: OtS_Triangle, voxelMesh: OtS_VoxelMesh) {
|
||||
const bounds = Triangle.CalcBounds(triangle.data.v0.position, triangle.data.v1.position, triangle.data.v2.position);
|
||||
bounds.min.floor();
|
||||
bounds.max.ceil();
|
||||
|
||||
const rayOrigin = new Vector3(0, 0, 0);
|
||||
|
||||
const edge1 = Vector3.sub(triangle.data.v1.position, triangle.data.v0.position);
|
||||
const edge2 = Vector3.sub(triangle.data.v2.position, triangle.data.v0.position);
|
||||
|
||||
const rasterisePlane = (a0: 'x' | 'y' | 'z', a1: 'x' | 'y' | 'z', a2: 'x' | 'y' | 'z', intersect: RayIntersect) => {
|
||||
rayOrigin[a0] = bounds.min[a0] - 1;
|
||||
for (let y = bounds.min[a1]; y <= bounds.max[a1]; ++y) {
|
||||
rayOrigin[a1] = y;
|
||||
let hasHit = false;
|
||||
|
||||
const start = findFirstTrueIndex(bounds.max[a2] - bounds.min[a2] + 1, (index: number) => {
|
||||
rayOrigin[a2] = bounds.min[a2] + index;
|
||||
return intersect(rayOrigin, triangle, edge1, edge2) !== undefined;
|
||||
});
|
||||
|
||||
for (let z = bounds.min[a2] + start; z <= bounds.max[a2]; ++z) {
|
||||
rayOrigin[a2] = z;
|
||||
const intersection = intersect(rayOrigin, triangle, edge1, edge2);
|
||||
if (intersection) {
|
||||
this._handleRayHit(intersection, triangle, voxelMesh);
|
||||
} else if (hasHit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rasterisePlane('x', 'y', 'z', rayIntersectTriangleFastX);
|
||||
rasterisePlane('y', 'z', 'x', rayIntersectTriangleFastY);
|
||||
rasterisePlane('z', 'x', 'y', rayIntersectTriangleFastZ);
|
||||
}
|
||||
|
||||
private _handleRayHit(intersection: Vector3, triangle: OtS_Triangle, voxelMesh: OtS_VoxelMesh) {
|
||||
const voxelPosition = new Vector3(
|
||||
intersection.x,
|
||||
intersection.y,
|
||||
intersection.z,
|
||||
).round();
|
||||
|
||||
let voxelColour: RGBA;
|
||||
if (this._config.multisampling !== undefined) {
|
||||
const samples: RGBA[] = [];
|
||||
for (let i = 0; i < this._config.multisampling; ++i) {
|
||||
samples.push(this._getVoxelColour(
|
||||
triangle,
|
||||
Vector3.random().divScalar(2.0).add(voxelPosition),
|
||||
))
|
||||
}
|
||||
voxelColour = RGBAUtil.average(...samples);
|
||||
} else {
|
||||
voxelColour = this._getVoxelColour(
|
||||
triangle,
|
||||
voxelPosition,
|
||||
);
|
||||
}
|
||||
|
||||
voxelMesh.addVoxel(voxelPosition.x, voxelPosition.y, voxelPosition.z, voxelColour, this._config.replaceMode);
|
||||
}
|
||||
|
||||
private _getVoxelColour(triangle: OtS_Triangle, location: Vector3): RGBA {
|
||||
if (triangle.type === 'solid') {
|
||||
return triangle.colour;
|
||||
}
|
||||
|
||||
const area01 = Triangle.CalcArea(triangle.data.v0.position, triangle.data.v1.position, location);
|
||||
const area12 = Triangle.CalcArea(triangle.data.v1.position, triangle.data.v2.position, location);
|
||||
const area20 = Triangle.CalcArea(triangle.data.v2.position, triangle.data.v0.position, location);
|
||||
const total = area01 + area12 + area20;
|
||||
|
||||
const w0 = area12 / total;
|
||||
const w1 = area20 / total;
|
||||
const w2 = area01 / total;
|
||||
|
||||
if (triangle.type === 'coloured') {
|
||||
return {
|
||||
r: triangle.data.v0.colour.r * w0 + triangle.data.v1.colour.r * w1 * triangle.data.v2.colour.r * w2,
|
||||
g: triangle.data.v0.colour.g * w0 + triangle.data.v1.colour.g * w1 * triangle.data.v2.colour.g * w2,
|
||||
b: triangle.data.v0.colour.b * w0 + triangle.data.v1.colour.b * w1 * triangle.data.v2.colour.b * w2,
|
||||
a: triangle.data.v0.colour.a * w0 + triangle.data.v1.colour.a * w1 * triangle.data.v2.colour.a * w2,
|
||||
};
|
||||
}
|
||||
|
||||
const texcoord: UV = {
|
||||
u: triangle.data.v0.texcoord.u * w0 + triangle.data.v1.texcoord.u * w1 + triangle.data.v2.texcoord.u * w2,
|
||||
v: triangle.data.v0.texcoord.v * w0 + triangle.data.v1.texcoord.v * w1 + triangle.data.v2.texcoord.v * w2,
|
||||
};
|
||||
|
||||
if (isNaN(texcoord.u) || isNaN(texcoord.v)) {
|
||||
return OtS_Colours.MAGENTA;
|
||||
}
|
||||
|
||||
return triangle.texture.sample(texcoord.u, texcoord.v);
|
||||
}
|
||||
|
||||
private _calcScaleOffset(mesh: OtS_Mesh) {
|
||||
const dimensions = mesh.calcBounds().getDimensions();
|
||||
|
||||
switch (this._config.constraintAxis) {
|
||||
case 'x':
|
||||
return {
|
||||
scale: (this._config.size - 1) / dimensions.x,
|
||||
offset: (this._config.size % 2 === 0) ? new Vector3(0.5, 0.0, 0.0) : new Vector3(0.0, 0.0, 0.0),
|
||||
}
|
||||
case 'y':
|
||||
return {
|
||||
scale: (this._config.size - 1) / dimensions.y,
|
||||
offset: (this._config.size % 2 === 0) ? new Vector3(0.0, 0.5, 0.0) : new Vector3(0.0, 0.0, 0.0),
|
||||
}
|
||||
case 'z':
|
||||
return {
|
||||
scale: (this._config.size - 1) / dimensions.z,
|
||||
offset: (this._config.size % 2 === 0) ? new Vector3(0.0, 0.0, 0.5) : new Vector3(0.0, 0.0, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
174
Core/src/ots_voxel_mesh_neighbourhood.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import { OtS_VoxelMesh } from "./ots_voxel_mesh";
|
||||
import { Vector3 } from "./vector";
|
||||
|
||||
export type OtS_Offset = -1 | 0 | 1;
|
||||
export type OtS_NeighbourhoodMode = 'cardinal' | 'non-cardinal';
|
||||
export enum OtS_FaceVisibility {
|
||||
None = 0,
|
||||
Up = 1 << 0,
|
||||
Down = 1 << 1,
|
||||
North = 1 << 2,
|
||||
East = 1 << 3,
|
||||
South = 1 << 4,
|
||||
West = 1 << 5,
|
||||
Full = Up | Down | North | East | South | West,
|
||||
}
|
||||
|
||||
/**
|
||||
* A util class to cache the voxel neighbours of a VoxelMesh
|
||||
*/
|
||||
export class OtS_VoxelMesh_Neighbourhood {
|
||||
private _voxelNeighbours: Map<number, number>;
|
||||
private _mode: OtS_NeighbourhoodMode | null;
|
||||
|
||||
public constructor() {
|
||||
this._voxelNeighbours = new Map();
|
||||
this._mode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the neighbourhood calculations, i.e. will go through voxel-by-voxel,
|
||||
* and cache what neighbours exist for each voxel. *Which* neighbours the
|
||||
* algorithm checks is determined by the 'mode':
|
||||
* - cardinal: Only checks 6 neighbours (the up/down/north/south/east/west directions)
|
||||
* - full: Checks all 26 three-dimensional neighbours
|
||||
*
|
||||
* @note `process` takes a snapshot of the current state of voxelMesh and
|
||||
* will not update if more voxels are added to voxelMesh or the state of
|
||||
* voxelMesh changes in any other way.
|
||||
*/
|
||||
public process(voxelMesh: OtS_VoxelMesh, mode: OtS_NeighbourhoodMode) {
|
||||
this._voxelNeighbours.clear();
|
||||
this._mode = mode;
|
||||
|
||||
const neighboursToCheck = mode === 'cardinal'
|
||||
? OtS_VoxelMesh_Neighbourhood._NEIGHBOURS_CARDINAL
|
||||
: OtS_VoxelMesh_Neighbourhood._NEIGHBOURS_NON_CARDINAL;
|
||||
|
||||
const pos = new Vector3(0, 0, 0);
|
||||
for (const voxel of voxelMesh.getVoxels()) {
|
||||
let neighbourValue = 0;
|
||||
|
||||
neighboursToCheck.forEach((neighbour) => {
|
||||
pos.setFrom(voxel.position);
|
||||
pos.add(neighbour.offset);
|
||||
|
||||
if (voxelMesh.isOpaqueVoxelAt(pos.x, pos.y, pos.z)) {
|
||||
neighbourValue |= (1 << neighbour.index);
|
||||
}
|
||||
})
|
||||
|
||||
this._voxelNeighbours.set(voxel.position.hash(), neighbourValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an encoded value representing the neighbours of the voxel at this
|
||||
* position. This is a confusing value to decode so instead use `hasNeighbour`
|
||||
* for checking if
|
||||
*/
|
||||
public getNeighbours(x: number, y: number, z: number): number {
|
||||
const key = Vector3.Hash(x, y, z);
|
||||
const value = this._voxelNeighbours.get(key);
|
||||
return value === undefined ? 0 : value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if a voxel at position has a neighbour with offset 'offset'
|
||||
*/
|
||||
public hasNeighbour(x: number, y: number, z: number, offsetX: OtS_Offset, offsetY: OtS_Offset, offsetZ: OtS_Offset): boolean {
|
||||
return (this.getNeighbours(x, y, z) & (1 << OtS_VoxelMesh_Neighbourhood.getNeighbourIndex(offsetX, offsetY, offsetZ))) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not you can see each face on a voxel at a given location
|
||||
*/
|
||||
public getFaceVisibility(x: number, y: number, z: number): (OtS_FaceVisibility | null) {
|
||||
if (this._mode !== 'cardinal') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let visibility: OtS_FaceVisibility = OtS_FaceVisibility.None;
|
||||
|
||||
if (!this.hasNeighbour(x, y, z, 1, 0, 0)) {
|
||||
visibility += OtS_FaceVisibility.North;
|
||||
}
|
||||
if (!this.hasNeighbour(x, y, z, -1, 0, 0)) {
|
||||
visibility += OtS_FaceVisibility.South;
|
||||
}
|
||||
if (!this.hasNeighbour(x, y, z, 0, 1, 0)) {
|
||||
visibility += OtS_FaceVisibility.Up;
|
||||
}
|
||||
if (!this.hasNeighbour(x, y, z, 0, -1, 0)) {
|
||||
visibility += OtS_FaceVisibility.Down;
|
||||
}
|
||||
if (!this.hasNeighbour(x, y, z, 0, 0, 1)) {
|
||||
visibility += OtS_FaceVisibility.East;
|
||||
}
|
||||
if (!this.hasNeighbour(x, y, z, 0, 0, -1)) {
|
||||
visibility += OtS_FaceVisibility.West;
|
||||
}
|
||||
|
||||
return visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mode that the data cached was built using.
|
||||
* Useful for debugging/testing.
|
||||
*/
|
||||
public getModeProcessedUsing() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
public static getNeighbourIndex(x: OtS_Offset, y: OtS_Offset, z: OtS_Offset) {
|
||||
return 9 * (x + 1) + 3 * (y + 1) + (z + 1);
|
||||
}
|
||||
|
||||
private static readonly _NEIGHBOURS_NON_CARDINAL = [
|
||||
new Vector3(1, 1, -1),
|
||||
new Vector3(0, 1, -1),
|
||||
new Vector3(-1, 1, -1),
|
||||
new Vector3(1, 0, -1),
|
||||
new Vector3(-1, 0, -1),
|
||||
new Vector3(1, -1, -1),
|
||||
new Vector3(0, -1, -1),
|
||||
new Vector3(-1, -1, -1),
|
||||
new Vector3(1, 1, 0),
|
||||
new Vector3(-1, 1, 0),
|
||||
new Vector3(1, -1, 0),
|
||||
new Vector3(-1, -1, 0),
|
||||
new Vector3(1, 1, 1),
|
||||
new Vector3(0, 1, 1),
|
||||
new Vector3(-1, 1, 1),
|
||||
new Vector3(1, 0, 1),
|
||||
new Vector3(-1, 0, 1),
|
||||
new Vector3(1, -1, 1),
|
||||
new Vector3(0, -1, 1),
|
||||
new Vector3(-1, -1, 1),
|
||||
].map((neighbourOffset) => {
|
||||
const inverseOffset = neighbourOffset.copy().negate();
|
||||
|
||||
return {
|
||||
offset: neighbourOffset,
|
||||
index: OtS_VoxelMesh_Neighbourhood.getNeighbourIndex(neighbourOffset.x as OtS_Offset, neighbourOffset.y as OtS_Offset, neighbourOffset.z as OtS_Offset),
|
||||
inverseIndex: OtS_VoxelMesh_Neighbourhood.getNeighbourIndex(inverseOffset.x as OtS_Offset, inverseOffset.y as OtS_Offset, inverseOffset.z as OtS_Offset),
|
||||
};
|
||||
});
|
||||
|
||||
private static readonly _NEIGHBOURS_CARDINAL = [
|
||||
new Vector3(1, 0, 0),
|
||||
new Vector3(-1, 0, 0),
|
||||
new Vector3(0, 1, 0),
|
||||
new Vector3(0, -1, 0),
|
||||
new Vector3(0, 0, 1),
|
||||
new Vector3(0, 0, -1),
|
||||
].map((neighbourOffset) => {
|
||||
const inverseOffset = neighbourOffset.copy().negate();
|
||||
|
||||
return {
|
||||
offset: neighbourOffset,
|
||||
index: OtS_VoxelMesh_Neighbourhood.getNeighbourIndex(neighbourOffset.x as OtS_Offset, neighbourOffset.y as OtS_Offset, neighbourOffset.z as OtS_Offset),
|
||||
inverseIndex: OtS_VoxelMesh_Neighbourhood.getNeighbourIndex(inverseOffset.x as OtS_Offset, inverseOffset.y as OtS_Offset, inverseOffset.z as OtS_Offset),
|
||||
};
|
||||
});
|
||||
}
|
130
Core/src/ray.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { OtS_Triangle } from './ots_mesh';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3 } from './vector';
|
||||
|
||||
const EPSILON = 0.0000001;
|
||||
|
||||
/* eslint-disable */
|
||||
export enum Axes {
|
||||
x, y, z,
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export function axesToDirection(axis: Axes) {
|
||||
if (axis === Axes.x) {
|
||||
return new Vector3(1, 0, 0);
|
||||
}
|
||||
if (axis === Axes.y) {
|
||||
return new Vector3(0, 1, 0);
|
||||
}
|
||||
if (axis === Axes.z) {
|
||||
return new Vector3(0, 0, 1);
|
||||
}
|
||||
ASSERT(false);
|
||||
}
|
||||
|
||||
export interface Ray {
|
||||
origin: Vector3,
|
||||
axis: Axes
|
||||
}
|
||||
|
||||
export type RayIntersect = (origin: Vector3, triangle: OtS_Triangle, edge1: Vector3, edge2: Vector3) => (Vector3 | undefined);
|
||||
|
||||
export function rayIntersectTriangleFastX(origin: Vector3, triangle: OtS_Triangle, edge1: Vector3, edge2: Vector3): (Vector3 | undefined) {
|
||||
const h = new Vector3(0, -edge2.z, edge2.y); // Vector3.cross(rayDirection, edge2);
|
||||
const a = Vector3.dot(edge1, h);
|
||||
|
||||
if (a > -EPSILON && a < EPSILON) {
|
||||
return; // Ray is parallel to triangle
|
||||
}
|
||||
|
||||
const f = 1.0 / a;
|
||||
const s = Vector3.sub(origin, triangle.data.v0.position);
|
||||
const u = f * Vector3.dot(s, h);
|
||||
|
||||
if (u < 0.0 || u > 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const q = Vector3.cross(s, edge1);
|
||||
const v = f * q.x; // f * Vector3.dot(rayDirection, q);
|
||||
|
||||
if (v < 0.0 || u + v > 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const t = f * Vector3.dot(edge2, q);
|
||||
|
||||
if (t > EPSILON) {
|
||||
const result = Vector3.copy(origin);
|
||||
result.x += t;
|
||||
return result;
|
||||
//return Vector3.add(origin, Vector3.mulScalar(rayDirection, t));
|
||||
}
|
||||
}
|
||||
|
||||
export function rayIntersectTriangleFastY(origin: Vector3, triangle: OtS_Triangle, edge1: Vector3, edge2: Vector3): (Vector3 | undefined) {
|
||||
const h = new Vector3(edge2.z, 0, -edge2.x); // Vector3.cross(rayDirection, edge2);
|
||||
const a = Vector3.dot(edge1, h);
|
||||
|
||||
if (a > -EPSILON && a < EPSILON) {
|
||||
return; // Ray is parallel to triangle
|
||||
}
|
||||
|
||||
const f = 1.0 / a;
|
||||
const s = Vector3.sub(origin, triangle.data.v0.position);
|
||||
const u = f * Vector3.dot(s, h);
|
||||
|
||||
if (u < 0.0 || u > 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const q = Vector3.cross(s, edge1);
|
||||
const v = f * q.y; // f * Vector3.dot(rayDirection, q);
|
||||
|
||||
if (v < 0.0 || u + v > 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const t = f * Vector3.dot(edge2, q);
|
||||
|
||||
if (t > EPSILON) {
|
||||
const result = Vector3.copy(origin);
|
||||
result.y += t;
|
||||
return result;
|
||||
//return Vector3.add(origin, Vector3.mulScalar(rayDirection, t));
|
||||
}
|
||||
}
|
||||
|
||||
export function rayIntersectTriangleFastZ(origin: Vector3, triangle: OtS_Triangle, edge1: Vector3, edge2: Vector3): (Vector3 | undefined) {
|
||||
const h = new Vector3(-edge2.y, edge2.x, 0); // Vector3.cross(rayDirection, edge2);
|
||||
const a = Vector3.dot(edge1, h);
|
||||
|
||||
if (a > -EPSILON && a < EPSILON) {
|
||||
return; // Ray is parallel to triangle
|
||||
}
|
||||
|
||||
const f = 1.0 / a;
|
||||
const s = Vector3.sub(origin, triangle.data.v0.position);
|
||||
const u = f * Vector3.dot(s, h);
|
||||
|
||||
if (u < 0.0 || u > 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const q = Vector3.cross(s, edge1);
|
||||
const v = f * q.z; // f * Vector3.dot(rayDirection, q);
|
||||
|
||||
if (v < 0.0 || u + v > 1.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const t = f * Vector3.dot(edge2, q);
|
||||
|
||||
if (t > EPSILON) {
|
||||
const result = Vector3.copy(origin);
|
||||
result.z += t;
|
||||
return result;
|
||||
//return Vector3.add(origin, Vector3.mulScalar(rayDirection, t));
|
||||
}
|
||||
}
|
37
Core/src/triangle.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Bounds } from './bounds';
|
||||
import { Vector3 } from './vector';
|
||||
|
||||
export class Triangle {
|
||||
public static CalcCentre(v0: Vector3, v1: Vector3, v2: Vector3): Vector3 {
|
||||
return Vector3.divScalar(Vector3.add(Vector3.add(v0, v1), v2), 3.0);
|
||||
}
|
||||
|
||||
public static CalcArea(v0: Vector3, v1: Vector3, v2: Vector3) {
|
||||
const a = Vector3.Distance(v0, v1);
|
||||
const b = Vector3.Distance(v1, v2);
|
||||
const c = Vector3.Distance(v2, v0);
|
||||
const p = (a + b + c) / 2;
|
||||
return Math.sqrt(p * (p - a) * (p - b) * (p - c));
|
||||
}
|
||||
|
||||
public static CalcNormal(v0: Vector3, v1: Vector3, v2: Vector3) {
|
||||
const u = Vector3.sub(v0, v1);
|
||||
const v = Vector3.sub(v0, v2);
|
||||
return Vector3.cross(u, v).normalise();
|
||||
}
|
||||
|
||||
public static CalcBounds(v0: Vector3, v1: Vector3, v2: Vector3): Bounds {
|
||||
return new Bounds(
|
||||
new Vector3(
|
||||
Math.min(v0.x, v1.x, v2.x),
|
||||
Math.min(v0.y, v1.y, v2.y),
|
||||
Math.min(v0.z, v1.z, v2.z),
|
||||
),
|
||||
new Vector3(
|
||||
Math.max(v0.x, v1.x, v2.x),
|
||||
Math.max(v0.y, v1.y, v2.y),
|
||||
Math.max(v0.z, v1.z, v2.z),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,85 +1,67 @@
|
||||
import { AppMath } from "./math";
|
||||
|
||||
export namespace AppUtil {
|
||||
export namespace Text {
|
||||
export function capitaliseFirstLetter(text: string) {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespaces a block name if it is not already namespaced
|
||||
* For example `namespaceBlock('stone')` returns `'minecraft:stone'`
|
||||
*/
|
||||
export function namespaceBlock(blockName: string): AppTypes.TNamespacedBlockName {
|
||||
// https://minecraft.fandom.com/wiki/Resource_location#Namespaces
|
||||
return isNamespacedBlock(blockName) ? blockName : ('minecraft:' + blockName);
|
||||
}
|
||||
|
||||
export function isNamespacedBlock(blockName: string): boolean {
|
||||
return blockName.includes(':');
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Array {
|
||||
|
||||
/**
|
||||
* An optimised function for repeating a subarray contained within a buffer multiple times by
|
||||
* repeatedly doubling the subarray's length.
|
||||
*/
|
||||
export function repeatedFill(buffer: Float32Array, start: number, startLength: number, desiredCount: number) {
|
||||
const pow = AppMath.largestPowerOfTwoLessThanN(desiredCount);
|
||||
|
||||
let len = startLength;
|
||||
for (let i = 0; i < pow; ++i) {
|
||||
buffer.copyWithin(start + len, start, start + len);
|
||||
len *= 2;
|
||||
}
|
||||
|
||||
const finalLength = desiredCount * startLength;
|
||||
buffer.copyWithin(start + len, start, start + finalLength - len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export enum EAction {
|
||||
Settings = 0,
|
||||
Import = 1,
|
||||
Materials = 2,
|
||||
Voxelise = 3,
|
||||
Assign = 4,
|
||||
Export = 5,
|
||||
MAX = 6,
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export namespace AppTypes {
|
||||
export type TNamespacedBlockName = string;
|
||||
}
|
||||
|
||||
export class UV {
|
||||
public u: number;
|
||||
public v: number;
|
||||
|
||||
constructor(u: number, v: number) {
|
||||
this.u = u;
|
||||
this.v = v;
|
||||
}
|
||||
|
||||
public copy() {
|
||||
return new UV(this.u, this.v);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export enum ColourSpace {
|
||||
RGB,
|
||||
LAB
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export type TOptional<T> = T | undefined;
|
||||
|
||||
export function getRandomID(): string {
|
||||
return (Math.random() + 1).toString(36).substring(7);
|
||||
}
|
||||
import { AppMath } from "./math";
|
||||
import { TBrand } from "./util/type_util";
|
||||
|
||||
export namespace AppUtil {
|
||||
export namespace Text {
|
||||
export function capitaliseFirstLetter(text: string) {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespaces a block name if it is not already namespaced
|
||||
* For example `namespaceBlock('stone')` returns `'minecraft:stone'`
|
||||
*/
|
||||
export function namespaceBlock(blockName: string): AppTypes.TNamespacedBlockName {
|
||||
// https://minecraft.fandom.com/wiki/Resource_location#Namespaces
|
||||
return isNamespacedBlock(blockName) ? blockName : ('minecraft:' + blockName);
|
||||
}
|
||||
|
||||
export function isNamespacedBlock(blockName: string): boolean {
|
||||
return blockName.includes(':');
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Array {
|
||||
|
||||
/**
|
||||
* An optimised function for repeating a subarray contained within a buffer multiple times by
|
||||
* repeatedly doubling the subarray's length.
|
||||
*/
|
||||
export function repeatedFill(buffer: Float32Array, start: number, startLength: number, desiredCount: number) {
|
||||
const pow = AppMath.largestPowerOfTwoLessThanN(desiredCount);
|
||||
|
||||
let len = startLength;
|
||||
for (let i = 0; i < pow; ++i) {
|
||||
buffer.copyWithin(start + len, start, start + len);
|
||||
len *= 2;
|
||||
}
|
||||
|
||||
const finalLength = desiredCount * startLength;
|
||||
buffer.copyWithin(start + len, start, start + finalLength - len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export enum EAction {
|
||||
Settings = 0,
|
||||
Import = 1,
|
||||
Materials = 2,
|
||||
Voxelise = 3,
|
||||
Assign = 4,
|
||||
Export = 5,
|
||||
MAX = 6,
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export namespace AppTypes {
|
||||
export type TNamespacedBlockName = string;
|
||||
}
|
||||
|
||||
export type UV = { u: number, v: number };
|
||||
|
||||
export type TOptional<T> = T | undefined;
|
||||
|
||||
export function getRandomID(): string {
|
||||
return (Math.random() + 1).toString(36).substring(7);
|
||||
}
|
18
Core/src/util/array_util.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function findFirstTrueIndex(length: number, valueAt: (index: number) => boolean) {
|
||||
let low = 0;
|
||||
let high = length - 1;
|
||||
let result = -1;
|
||||
|
||||
while (low <= high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
|
||||
if (valueAt(mid) === true) {
|
||||
result = mid;
|
||||
high = mid - 1;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
6
Core/src/util/error_util.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function ASSERT(condition: any, errorMessage: string = 'Assertion Failed'): asserts condition {
|
||||
if (!condition) {
|
||||
Error(errorMessage);
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
}
|
@ -1,8 +1,3 @@
|
||||
import util from 'util';
|
||||
|
||||
import { AppConfig } from '../config';
|
||||
import { AppPaths, PathUtil } from './path_util';
|
||||
|
||||
/**
|
||||
* Logs to console and file if logging `LOG` is enabled.
|
||||
* This should be used for verbose logs.
|
@ -1,7 +1,7 @@
|
||||
import { NBT, writeUncompressed } from 'prismarine-nbt';
|
||||
import zlib from 'zlib';
|
||||
|
||||
export function saveNBT(nbt: NBT) {
|
||||
const uncompressedBuffer = writeUncompressed(nbt, 'big');
|
||||
return zlib.gzipSync(uncompressedBuffer);
|
||||
}
|
||||
import { NBT, writeUncompressed } from 'prismarine-nbt';
|
||||
import pako from 'pako';
|
||||
|
||||
export function saveNBT(nbt: NBT) {
|
||||
const uncompressedBuffer = writeUncompressed(nbt, 'big');
|
||||
return pako.gzip(uncompressedBuffer);
|
||||
}
|
24
Core/src/util/type_util.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export type TBrand<K, T> = K & { __brand: T };
|
||||
|
||||
export type Vector3Hash = TBrand<number, 'Vector3Hash'>;
|
||||
|
||||
export type TDithering = 'off' | 'random' | 'ordered';
|
||||
|
||||
export type TAxis = 'x' | 'y' | 'z';
|
||||
|
||||
export type TTexelExtension = 'repeat' | 'clamp';
|
||||
|
||||
export type TTexelInterpolation = 'nearest' | 'linear';
|
||||
|
||||
export type WrappedWarnings = {};
|
||||
|
||||
export type WrappedInfo = {}
|
||||
|
||||
/** Wrapped simply wraps a payload with a list of warnings/info associated with it */
|
||||
export type Wrapped<T> = { payload: T, warnings: WrappedWarnings[], info: WrappedInfo[] };
|
||||
|
||||
export type BlockPalette = Set<string>;
|
||||
|
||||
export type Result<T, E = Error> =
|
||||
| { ok: true, value: T }
|
||||
| { ok: false, error: { code: E, message?: string } };
|
@ -1,270 +1,275 @@
|
||||
import { IHashable } from './hash_map';
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3Hash } from './util/type_util';
|
||||
|
||||
export class Vector3 implements IHashable {
|
||||
public x: number;
|
||||
public y: number;
|
||||
public z: number;
|
||||
|
||||
constructor(x: number, y: number, z: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public set(x: number, y: number, z: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public setFrom(vec: Vector3) {
|
||||
this.x = vec.x;
|
||||
this.y = vec.y;
|
||||
this.z = vec.z;
|
||||
}
|
||||
|
||||
static fromArray(arr: number[]) {
|
||||
ASSERT(arr.length === 3);
|
||||
return new Vector3(arr[0], arr[1], arr[2]);
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return [this.x, this.y, this.z];
|
||||
}
|
||||
|
||||
static random() {
|
||||
return new Vector3(
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
);
|
||||
}
|
||||
|
||||
static parse(line: string) {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const floats = line.match(regex)!.map(function(v) {
|
||||
return parseFloat(v);
|
||||
});
|
||||
|
||||
return new Vector3(
|
||||
floats[0], floats[1], floats[2],
|
||||
);
|
||||
}
|
||||
|
||||
public static copy(vec: Vector3) {
|
||||
return new Vector3(
|
||||
vec.x,
|
||||
vec.y,
|
||||
vec.z,
|
||||
);
|
||||
}
|
||||
|
||||
public static add(vec: Vector3, toAdd: (Vector3 | number)) {
|
||||
return Vector3.copy(vec).add(toAdd);
|
||||
}
|
||||
|
||||
public static sub(vec: Vector3, toAdd: (Vector3 | number)) {
|
||||
return Vector3.copy(vec).sub(toAdd);
|
||||
}
|
||||
|
||||
public add(toAdd: (Vector3 | number)) {
|
||||
if (toAdd instanceof Vector3) {
|
||||
this.x += toAdd.x;
|
||||
this.y += toAdd.y;
|
||||
this.z += toAdd.z;
|
||||
return this;
|
||||
} else {
|
||||
this.x += toAdd;
|
||||
this.y += toAdd;
|
||||
this.z += toAdd;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sub(toAdd: (Vector3 | number)) {
|
||||
if (toAdd instanceof Vector3) {
|
||||
this.x -= toAdd.x;
|
||||
this.y -= toAdd.y;
|
||||
this.z -= toAdd.z;
|
||||
return this;
|
||||
} else {
|
||||
this.x -= toAdd;
|
||||
this.y -= toAdd;
|
||||
this.z -= toAdd;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
static dot(vecA: Vector3, vecB: Vector3) {
|
||||
return vecA.x * vecB.x + vecA.y * vecB.y + vecA.z * vecB.z;
|
||||
}
|
||||
|
||||
public copy() {
|
||||
return Vector3.copy(this);
|
||||
}
|
||||
|
||||
static mulScalar(vec: Vector3, scalar: number) {
|
||||
return new Vector3(
|
||||
scalar * vec.x,
|
||||
scalar * vec.y,
|
||||
scalar * vec.z,
|
||||
);
|
||||
}
|
||||
|
||||
mulScalar(scalar: number) {
|
||||
this.x *= scalar;
|
||||
this.y *= scalar;
|
||||
this.z *= scalar;
|
||||
return this;
|
||||
}
|
||||
|
||||
static divScalar(vec: Vector3, scalar: number) {
|
||||
return new Vector3(
|
||||
vec.x / scalar,
|
||||
vec.y / scalar,
|
||||
vec.z / scalar,
|
||||
);
|
||||
}
|
||||
|
||||
divScalar(scalar: number) {
|
||||
this.x /= scalar;
|
||||
this.y /= scalar;
|
||||
this.z /= scalar;
|
||||
return this;
|
||||
}
|
||||
|
||||
static lessThanEqualTo(vecA: Vector3, vecB: Vector3) {
|
||||
return vecA.x <= vecB.x && vecA.y <= vecB.y && vecA.z <= vecB.z;
|
||||
}
|
||||
|
||||
static round(vec: Vector3) {
|
||||
return new Vector3(
|
||||
Math.round(vec.x),
|
||||
Math.round(vec.y),
|
||||
Math.round(vec.z),
|
||||
);
|
||||
}
|
||||
|
||||
round() {
|
||||
this.x = Math.round(this.x);
|
||||
this.y = Math.round(this.y);
|
||||
this.z = Math.round(this.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
static abs(vec: Vector3) {
|
||||
return new Vector3(
|
||||
Math.abs(vec.x),
|
||||
Math.abs(vec.y),
|
||||
Math.abs(vec.z),
|
||||
);
|
||||
}
|
||||
|
||||
static cross(vecA: Vector3, vecB: Vector3) {
|
||||
return new Vector3(
|
||||
vecA.y * vecB.z - vecA.z * vecB.y,
|
||||
vecA.z * vecB.x - vecA.x * vecB.z,
|
||||
vecA.x * vecB.y - vecA.y * vecB.x,
|
||||
);
|
||||
}
|
||||
|
||||
static min(vecA: Vector3, vecB: Vector3) {
|
||||
return new Vector3(
|
||||
Math.min(vecA.x, vecB.x),
|
||||
Math.min(vecA.y, vecB.y),
|
||||
Math.min(vecA.z, vecB.z),
|
||||
);
|
||||
}
|
||||
|
||||
static max(vecA: Vector3, vecB: Vector3) {
|
||||
return new Vector3(
|
||||
Math.max(vecA.x, vecB.x),
|
||||
Math.max(vecA.y, vecB.y),
|
||||
Math.max(vecA.z, vecB.z),
|
||||
);
|
||||
}
|
||||
|
||||
magnitude() {
|
||||
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
|
||||
}
|
||||
|
||||
normalise() {
|
||||
const mag = this.magnitude();
|
||||
this.x /= mag;
|
||||
this.y /= mag;
|
||||
this.z /= mag;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public static get xAxis() {
|
||||
return new Vector3(1.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
public static get yAxis() {
|
||||
return new Vector3(0.0, 1.0, 0.0);
|
||||
}
|
||||
|
||||
public static get zAxis() {
|
||||
return new Vector3(0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
public isNumber() {
|
||||
return !isNaN(this.x) && !isNaN(this.y) && !isNaN(this.z);
|
||||
}
|
||||
|
||||
public negate() {
|
||||
this.x = -this.x;
|
||||
this.y = -this.y;
|
||||
this.z = -this.z;
|
||||
return this;
|
||||
}
|
||||
|
||||
public floor() {
|
||||
this.x = Math.floor(this.x);
|
||||
this.y = Math.floor(this.y);
|
||||
this.z = Math.floor(this.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ceil() {
|
||||
this.x = Math.ceil(this.x);
|
||||
this.y = Math.ceil(this.y);
|
||||
this.z = Math.ceil(this.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Begin IHashable interface
|
||||
public hash(): Vector3Hash {
|
||||
return ((this.x + 10_000_000) << 42) + ((this.y + 10_000_000) << 21) + (this.z + 10_000_000) as Vector3Hash;
|
||||
}
|
||||
|
||||
public equals(other: Vector3) {
|
||||
return this.x == other.x && this.y == other.y && this.z == other.z;
|
||||
}
|
||||
// End IHashable interface
|
||||
|
||||
public stringify() {
|
||||
return `${this.x}_${this.y}_${this.z}`;
|
||||
}
|
||||
|
||||
public intoArray(array: Float32Array, start: number) {
|
||||
array[start + 0] = this.x;
|
||||
array[start + 1] = this.y;
|
||||
array[start + 2] = this.z;
|
||||
}
|
||||
}
|
||||
|
||||
export const fastCrossXAxis = (vec: Vector3) => {
|
||||
return new Vector3(0.0, -vec.z, vec.y);
|
||||
};
|
||||
|
||||
export const fastCrossYAxis = (vec: Vector3) => {
|
||||
return new Vector3(vec.z, 0.0, -vec.x);
|
||||
};
|
||||
|
||||
export const fastCrossZAxis = (vec: Vector3) => {
|
||||
return new Vector3(-vec.y, vec.x, 0.0);
|
||||
};
|
||||
import { ASSERT } from './util/error_util';
|
||||
import { Vector3Hash } from './util/type_util';
|
||||
|
||||
export class Vector3 {
|
||||
public x: number;
|
||||
public y: number;
|
||||
public z: number;
|
||||
|
||||
constructor(x: number, y: number, z: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public set(x: number, y: number, z: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public setFrom(vec: Vector3) {
|
||||
this.x = vec.x;
|
||||
this.y = vec.y;
|
||||
this.z = vec.z;
|
||||
}
|
||||
|
||||
static fromArray(arr: number[]) {
|
||||
ASSERT(arr.length === 3);
|
||||
return new Vector3(arr[0], arr[1], arr[2]);
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return [this.x, this.y, this.z];
|
||||
}
|
||||
|
||||
static random() {
|
||||
return new Vector3(
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
Math.random(),
|
||||
);
|
||||
}
|
||||
|
||||
static parse(line: string) {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const floats = line.match(regex)!.map(function(v) {
|
||||
return parseFloat(v);
|
||||
});
|
||||
|
||||
return new Vector3(
|
||||
floats[0], floats[1], floats[2],
|
||||
);
|
||||
}
|
||||
|
||||
public static copy(vec: Vector3) {
|
||||
return new Vector3(
|
||||
vec.x,
|
||||
vec.y,
|
||||
vec.z,
|
||||
);
|
||||
}
|
||||
|
||||
public static add(vec: Vector3, toAdd: (Vector3 | number)) {
|
||||
return Vector3.copy(vec).add(toAdd);
|
||||
}
|
||||
|
||||
public static sub(vec: Vector3, toAdd: (Vector3 | number)) {
|
||||
return Vector3.copy(vec).sub(toAdd);
|
||||
}
|
||||
|
||||
public add(toAdd: (Vector3 | number)) {
|
||||
if (toAdd instanceof Vector3) {
|
||||
this.x += toAdd.x;
|
||||
this.y += toAdd.y;
|
||||
this.z += toAdd.z;
|
||||
return this;
|
||||
} else {
|
||||
this.x += toAdd;
|
||||
this.y += toAdd;
|
||||
this.z += toAdd;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public sub(toAdd: (Vector3 | number)) {
|
||||
if (toAdd instanceof Vector3) {
|
||||
this.x -= toAdd.x;
|
||||
this.y -= toAdd.y;
|
||||
this.z -= toAdd.z;
|
||||
return this;
|
||||
} else {
|
||||
this.x -= toAdd;
|
||||
this.y -= toAdd;
|
||||
this.z -= toAdd;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
static dot(vecA: Vector3, vecB: Vector3) {
|
||||
return vecA.x * vecB.x + vecA.y * vecB.y + vecA.z * vecB.z;
|
||||
}
|
||||
|
||||
public copy() {
|
||||
return Vector3.copy(this);
|
||||
}
|
||||
|
||||
static mulScalar(vec: Vector3, scalar: number) {
|
||||
return new Vector3(
|
||||
scalar * vec.x,
|
||||
scalar * vec.y,
|
||||
scalar * vec.z,
|
||||
);
|
||||
}
|
||||
|
||||
mulScalar(scalar: number) {
|
||||
this.x *= scalar;
|
||||
this.y *= scalar;
|
||||
this.z *= scalar;
|
||||
return this;
|
||||
}
|
||||
|
||||
static divScalar(vec: Vector3, scalar: number) {
|
||||
return new Vector3(
|
||||
vec.x / scalar,
|
||||
vec.y / scalar,
|
||||
vec.z / scalar,
|
||||
);
|
||||
}
|
||||
|
||||
divScalar(scalar: number) {
|
||||
this.x /= scalar;
|
||||
this.y /= scalar;
|
||||
this.z /= scalar;
|
||||
return this;
|
||||
}
|
||||
|
||||
static lessThanEqualTo(vecA: Vector3, vecB: Vector3) {
|
||||
return vecA.x <= vecB.x && vecA.y <= vecB.y && vecA.z <= vecB.z;
|
||||
}
|
||||
|
||||
static round(vec: Vector3) {
|
||||
return new Vector3(
|
||||
Math.round(vec.x),
|
||||
Math.round(vec.y),
|
||||
Math.round(vec.z),
|
||||
);
|
||||
}
|
||||
|
||||
round() {
|
||||
this.x = Math.round(this.x);
|
||||
this.y = Math.round(this.y);
|
||||
this.z = Math.round(this.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
static abs(vec: Vector3) {
|
||||
return new Vector3(
|
||||
Math.abs(vec.x),
|
||||
Math.abs(vec.y),
|
||||
Math.abs(vec.z),
|
||||
);
|
||||
}
|
||||
|
||||
static cross(vecA: Vector3, vecB: Vector3) {
|
||||
return new Vector3(
|
||||
vecA.y * vecB.z - vecA.z * vecB.y,
|
||||
vecA.z * vecB.x - vecA.x * vecB.z,
|
||||
vecA.x * vecB.y - vecA.y * vecB.x,
|
||||
);
|
||||
}
|
||||
|
||||
static min(vecA: Vector3, vecB: Vector3) {
|
||||
return new Vector3(
|
||||
Math.min(vecA.x, vecB.x),
|
||||
Math.min(vecA.y, vecB.y),
|
||||
Math.min(vecA.z, vecB.z),
|
||||
);
|
||||
}
|
||||
|
||||
static max(vecA: Vector3, vecB: Vector3) {
|
||||
return new Vector3(
|
||||
Math.max(vecA.x, vecB.x),
|
||||
Math.max(vecA.y, vecB.y),
|
||||
Math.max(vecA.z, vecB.z),
|
||||
);
|
||||
}
|
||||
|
||||
magnitude() {
|
||||
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
|
||||
}
|
||||
|
||||
public static Distance(v0: Vector3, v1: Vector3): number {
|
||||
return Math.sqrt((v0.x - v1.x)*(v0.x - v1.x) + (v0.y - v1.y)*(v0.y - v1.y) + (v0.z - v1.z)*(v0.z - v1.z));
|
||||
}
|
||||
|
||||
normalise() {
|
||||
const mag = this.magnitude();
|
||||
this.x /= mag;
|
||||
this.y /= mag;
|
||||
this.z /= mag;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public static get xAxis() {
|
||||
return new Vector3(1.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
public static get yAxis() {
|
||||
return new Vector3(0.0, 1.0, 0.0);
|
||||
}
|
||||
|
||||
public static get zAxis() {
|
||||
return new Vector3(0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
public isNumber() {
|
||||
return !isNaN(this.x) && !isNaN(this.y) && !isNaN(this.z);
|
||||
}
|
||||
|
||||
public negate() {
|
||||
this.x = -this.x;
|
||||
this.y = -this.y;
|
||||
this.z = -this.z;
|
||||
return this;
|
||||
}
|
||||
|
||||
public floor() {
|
||||
this.x = Math.floor(this.x);
|
||||
this.y = Math.floor(this.y);
|
||||
this.z = Math.floor(this.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ceil() {
|
||||
this.x = Math.ceil(this.x);
|
||||
this.y = Math.ceil(this.y);
|
||||
this.z = Math.ceil(this.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public hash(): Vector3Hash {
|
||||
return Vector3.Hash(this.x, this.y, this.z) as Vector3Hash;
|
||||
}
|
||||
|
||||
public static Hash(x: number, y: number, z: number) {
|
||||
return ((x + 10_000_000) << 42) + ((y + 10_000_000) << 21) + (z + 10_000_000);
|
||||
}
|
||||
|
||||
public equals(other: Vector3) {
|
||||
return this.x == other.x && this.y == other.y && this.z == other.z;
|
||||
}
|
||||
|
||||
public stringify() {
|
||||
return `${this.x}_${this.y}_${this.z}`;
|
||||
}
|
||||
|
||||
public intoArray(array: Float32Array, start: number) {
|
||||
array[start + 0] = this.x;
|
||||
array[start + 1] = this.y;
|
||||
array[start + 2] = this.z;
|
||||
}
|
||||
}
|
||||
|
||||
export const fastCrossXAxis = (vec: Vector3) => {
|
||||
return new Vector3(0.0, -vec.z, vec.y);
|
||||
};
|
||||
|
||||
export const fastCrossYAxis = (vec: Vector3) => {
|
||||
return new Vector3(vec.z, 0.0, -vec.x);
|
||||
};
|
||||
|
||||
export const fastCrossZAxis = (vec: Vector3) => {
|
||||
return new Vector3(-vec.y, vec.x, 0.0);
|
||||
};
|
95
Core/tests/ots_block_mesh_converter.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { OtS_VoxelMesh } from '../src/ots_voxel_mesh';
|
||||
import { OtS_Colours, RGBAUtil } from '../src/colour';
|
||||
import { OtS_BlockMesh_Converter } from '../src/ots_block_mesh_converter';
|
||||
|
||||
test('Per-block', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.RED);
|
||||
voxelMesh.addVoxel(1, 0, 0, OtS_Colours.GREEN);
|
||||
voxelMesh.addVoxel(2, 0, 0, OtS_Colours.BLUE);
|
||||
|
||||
const converter = new OtS_BlockMesh_Converter();
|
||||
converter.setConfig({
|
||||
mode: {
|
||||
type: 'per-block', data: [
|
||||
{ name: 'RED-BLOCK', colour: OtS_Colours.RED },
|
||||
{ name: 'GREEN-BLOCK', colour: OtS_Colours.GREEN },
|
||||
{ name: 'BLUE-BLOCK', colour: OtS_Colours.BLUE }
|
||||
]
|
||||
},
|
||||
});
|
||||
|
||||
const blockMesh = converter.process(voxelMesh);
|
||||
|
||||
expect(blockMesh.isBlockAt(0, 0, 0)).toBe(true);
|
||||
expect(blockMesh.getBlockAt(0, 0, 0)?.name).toBe('RED-BLOCK');
|
||||
|
||||
expect(blockMesh.isBlockAt(1, 0, 0)).toBe(true);
|
||||
expect(blockMesh.getBlockAt(1, 0, 0)?.name).toBe('GREEN-BLOCK');
|
||||
|
||||
expect(blockMesh.isBlockAt(2, 0, 0)).toBe(true);
|
||||
expect(blockMesh.getBlockAt(2, 0, 0)?.name).toBe('BLUE-BLOCK');
|
||||
});
|
||||
|
||||
test('Per-face', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.RED);
|
||||
voxelMesh.addVoxel(0, -1, 0, OtS_Colours.BLUE);
|
||||
voxelMesh.addVoxel(1, 0, 0, OtS_Colours.BLUE);
|
||||
voxelMesh.addVoxel(-1, 0, 0, OtS_Colours.BLUE);
|
||||
voxelMesh.addVoxel(0, 0, 1, OtS_Colours.BLUE);
|
||||
voxelMesh.addVoxel(0, 0, -1, OtS_Colours.BLUE);
|
||||
|
||||
const converter = new OtS_BlockMesh_Converter();
|
||||
converter.setConfig({
|
||||
mode: {
|
||||
type: 'per-face', data: {
|
||||
blocks: [
|
||||
{
|
||||
name: 'RED-TOP-BLOCK',
|
||||
textures: {
|
||||
up: 'RED',
|
||||
down: 'BLACK',
|
||||
north: 'BLACK',
|
||||
south: 'BLACK',
|
||||
east: 'BLACK',
|
||||
west: 'BLACK',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'BLUE-BLOCK',
|
||||
textures: {
|
||||
up: 'BLUE',
|
||||
down: 'BLUE',
|
||||
north: 'BLUE',
|
||||
south: 'BLUE',
|
||||
east: 'BLUE',
|
||||
west: 'BLUE',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'KINDA-RED-BLOCK',
|
||||
textures: {
|
||||
up: 'KINDA-RED',
|
||||
down: 'KINDA-RED',
|
||||
north: 'KINDA-RED',
|
||||
south: 'KINDA-RED',
|
||||
east: 'KINDA-RED',
|
||||
west: 'KINDA-RED',
|
||||
},
|
||||
}
|
||||
],
|
||||
textures: {
|
||||
'RED': OtS_Colours.RED,
|
||||
'BLACK': OtS_Colours.BLACK,
|
||||
'BLUE': OtS_Colours.BLUE,
|
||||
'KINDA-RED': { r: 0.5, g: 0.0, b: 0.0, a: 1.0 },
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const blockMesh = converter.process(voxelMesh);
|
||||
expect(blockMesh.getBlockCount()).toBe(6);
|
||||
expect(blockMesh.getBlockAt(0, 0, 0)?.name).toBe('RED-TOP-BLOCK');
|
||||
});
|
56
Core/tests/ots_mesh.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { OtS_Mesh } from "../src/ots_mesh";
|
||||
import { OtS_Colours } from '../src/colour';
|
||||
|
||||
test('Mesh Triangles', () => {
|
||||
const mesh = OtS_Mesh.create();
|
||||
|
||||
mesh.addSection({
|
||||
name: 'Test Section 1',
|
||||
type: 'solid',
|
||||
colour: OtS_Colours.WHITE,
|
||||
positionData: Float32Array.from([
|
||||
0.0, 0.0, 0.0,
|
||||
1.0, 2.0, 3.0,
|
||||
4.0, 5.0, 6.0,
|
||||
0.0, 0.0, 0.0,
|
||||
1.0, 2.0, 3.0,
|
||||
4.0, 5.0, 6.0,
|
||||
]),
|
||||
normalData: Float32Array.from([
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
]),
|
||||
indexData: Uint32Array.from([
|
||||
0, 1, 2,
|
||||
3, 4, 5
|
||||
]),
|
||||
});
|
||||
|
||||
mesh.addSection({
|
||||
name: 'Test Section 2',
|
||||
type: 'solid',
|
||||
colour: OtS_Colours.WHITE,
|
||||
positionData: Float32Array.from([
|
||||
0.0, 0.0, 0.0,
|
||||
1.0, 2.0, 3.0,
|
||||
4.0, 5.0, 6.0,
|
||||
]),
|
||||
normalData: Float32Array.from([
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
]),
|
||||
indexData: Uint32Array.from([
|
||||
0, 1, 2,
|
||||
]),
|
||||
});
|
||||
|
||||
expect(mesh.calcTriangleCount()).toBe(3);
|
||||
|
||||
const triangles = Array.from(mesh.getTriangles());
|
||||
expect(triangles.length).toBe(3);
|
||||
});
|
115
Core/tests/ots_voxel_mesh.test.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { OtS_VoxelMesh } from '../src/ots_voxel_mesh';
|
||||
import { ASSERT } from '../src/util/error_util';
|
||||
import { Vector3 } from '../src/vector';
|
||||
|
||||
test('VoxelMesh #1', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
expect(voxelMesh.getVoxelCount()).toBe(0);
|
||||
expect(voxelMesh.getVoxelAt(0, 0, 0)).toBe(null);
|
||||
});
|
||||
|
||||
test('VoxelMesh #2', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 0.5, b: 0.25, a: 0.125 }, 'keep');
|
||||
expect(voxelMesh.getVoxelCount()).toBe(1);
|
||||
expect(voxelMesh.isVoxelAt(1, 2, 3)).toBe(true);
|
||||
expect(voxelMesh.isOpaqueVoxelAt(1, 2, 3)).toBe(false);
|
||||
|
||||
voxelMesh.addVoxel(-6, -6, -6, { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 'keep');
|
||||
expect(voxelMesh.getVoxelCount()).toBe(2);
|
||||
expect(voxelMesh.isVoxelAt(-6, -6, -6)).toBe(true);
|
||||
expect(voxelMesh.isOpaqueVoxelAt(-6, -6, -6)).toBe(true);
|
||||
});
|
||||
|
||||
test('VoxelMesh #3', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 0.5, b: 0.25, a: 0.125 }, 'keep');
|
||||
const voxel = voxelMesh.getVoxelAt(1, 2, 3);
|
||||
|
||||
expect(voxel === null).toBe(false);
|
||||
expect(voxel?.colour).toStrictEqual({ r: 1.0, g: 0.5, b: 0.25, a: 0.125 });
|
||||
expect(voxel?.position.equals(new Vector3(1, 2, 3))).toBe(true);
|
||||
});
|
||||
|
||||
test('VoxelMesh #4', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 0.5, b: 0.25, a: 0.125 }, 'keep');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 1.0, g: 0.5, b: 0.25, a: 0.125 });
|
||||
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 0.0, g: 0.1, b: 0.2, a: 0.3 }, 'keep');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 1.0, g: 0.5, b: 0.25, a: 0.125 })
|
||||
});
|
||||
|
||||
test('VoxelMesh #5', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 0.5, b: 0.25, a: 0.125 }, 'replace');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 1.0, g: 0.5, b: 0.25, a: 0.125 });
|
||||
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 0.0, g: 0.1, b: 0.2, a: 0.3 }, 'replace');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 0.0, g: 0.1, b: 0.2, a: 0.3 });
|
||||
});
|
||||
|
||||
test('VoxelMesh #6', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 0.5, b: 0.125, a: 1.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 1.0, g: 0.5, b: 0.125, a: 1.0 });
|
||||
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 0.5, g: 0.25, b: 0.0625, a: 0.5 });
|
||||
});
|
||||
|
||||
test('VoxelMesh #7', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 1.0, g: 1.0, b: 1.0, a: 1.0 });
|
||||
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 0.5, g: 0.5, b: 0.5, a: 0.5 });
|
||||
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 1 / 3, g: 1 / 3, b: 1 / 3, a: 1 / 3 });
|
||||
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelAt(1, 2, 3)?.colour).toStrictEqual({ r: 0.25, g: 0.25, b: 0.25, a: 0.25 });
|
||||
});
|
||||
|
||||
test('VoxelMesh #8', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 'average');
|
||||
expect(voxelMesh.getVoxelCount()).toBe(1);
|
||||
expect(voxelMesh.isVoxelAt(1, 2, 3)).toBe(true);
|
||||
|
||||
expect(voxelMesh.removeVoxel(1, 2, 3)).toBe(true);
|
||||
expect(voxelMesh.getVoxelCount()).toBe(0);
|
||||
expect(voxelMesh.isVoxelAt(1, 2, 3)).toBe(false);
|
||||
|
||||
expect(voxelMesh.removeVoxel(1, 2, 3)).toBe(false);
|
||||
});
|
||||
|
||||
test('VoxelMesh #9', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 2, 3, { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 'average');
|
||||
expect(voxelMesh.getBounds().getCentre().equals(new Vector3(1, 2, 3))).toBe(true);
|
||||
expect(voxelMesh.getBounds().getDimensions().equals(new Vector3(0, 0, 0))).toBe(true);
|
||||
|
||||
voxelMesh.addVoxel(3, 5, 7, { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 'average');
|
||||
expect(voxelMesh.getBounds().getDimensions().equals(new Vector3(2, 3, 4))).toBe(true);
|
||||
|
||||
voxelMesh.removeVoxel(3, 5, 7);
|
||||
expect(voxelMesh.getBounds().getDimensions().equals(new Vector3(0, 0, 0))).toBe(true);
|
||||
});
|
||||
|
||||
test('VoxelMesh #10', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(1, 0, 0, { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, 'replace');
|
||||
voxelMesh.addVoxel(0, 1, 0, { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, 'replace');
|
||||
voxelMesh.addVoxel(0, 0, 1, { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, 'replace');
|
||||
|
||||
let voxelsCount = 0;
|
||||
for (const voxel of voxelMesh.getVoxels()) {
|
||||
++voxelsCount;
|
||||
expect(voxel.position.equals(new Vector3(1, 0, 0)) || voxel.position.equals(new Vector3(0, 1, 0)) || voxel.position.equals(new Vector3(0, 0, 1))).toBe(true);
|
||||
}
|
||||
|
||||
expect(voxelsCount).toBe(3);
|
||||
});
|
109
Core/tests/ots_voxel_mesh_neighbourhood.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { OtS_Colours } from '../src/colour';
|
||||
import { OtS_VoxelMesh } from '../src/ots_voxel_mesh';
|
||||
import { OtS_FaceVisibility, OtS_VoxelMesh_Neighbourhood } from '../src/ots_voxel_mesh_neighbourhood';
|
||||
|
||||
test('VoxelMesh Neighbourhood #1', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'cardinal');
|
||||
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(OtS_FaceVisibility.Full);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #2', () => {
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(null);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #3', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(0, 1, 0, OtS_Colours.WHITE, 'replace');
|
||||
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'cardinal');
|
||||
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(OtS_FaceVisibility.Down | OtS_FaceVisibility.North | OtS_FaceVisibility.South | OtS_FaceVisibility.East | OtS_FaceVisibility.West);
|
||||
expect(neighbourhood.getFaceVisibility(0, 1, 0)).toBe(OtS_FaceVisibility.Up | OtS_FaceVisibility.North | OtS_FaceVisibility.South | OtS_FaceVisibility.East | OtS_FaceVisibility.West);
|
||||
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 0, 1, 0)).toBe(true);
|
||||
expect(neighbourhood.hasNeighbour(0, 1, 0, 0, -1, 0)).toBe(true);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #4', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(1, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(0, 1, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(0, 0, 1, OtS_Colours.WHITE, 'replace');
|
||||
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'cardinal');
|
||||
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(OtS_FaceVisibility.Down | OtS_FaceVisibility.South | OtS_FaceVisibility.West);
|
||||
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 0, 0)).toBe(true);
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 0, 1, 0)).toBe(true);
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 0, 0, 1)).toBe(true);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #5', () => {
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 0, 0)).toBe(false);
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 0, 1, 0)).toBe(false);
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 0, 0, 1)).toBe(false);
|
||||
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(null);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #6', () => {
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(1, 1, 1, OtS_Colours.WHITE, 'replace');
|
||||
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'non-cardinal');
|
||||
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 1, 1)).toBe(true);
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(null);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #6', () => {
|
||||
// Checking a non-cardinal neighbour when processing using 'cardinal' mode
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(1, 1, 1, OtS_Colours.WHITE, 'replace');
|
||||
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'cardinal');
|
||||
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 1, 1)).toBe(false);
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(OtS_FaceVisibility.Full);
|
||||
|
||||
// Now use proper mode
|
||||
neighbourhood.process(voxelMesh, 'non-cardinal');
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 1, 1)).toBe(true);
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(null);
|
||||
});
|
||||
|
||||
test('VoxelMesh Neighbourhood #6', () => {
|
||||
// Checking a cardinal neighbour when processing using 'non-cardinal' mode
|
||||
const voxelMesh = new OtS_VoxelMesh();
|
||||
voxelMesh.addVoxel(0, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
voxelMesh.addVoxel(1, 0, 0, OtS_Colours.WHITE, 'replace');
|
||||
|
||||
const neighbourhood = new OtS_VoxelMesh_Neighbourhood();
|
||||
neighbourhood.process(voxelMesh, 'non-cardinal');
|
||||
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 0, 0)).toBe(false);
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(null);
|
||||
|
||||
// Now use proper mode
|
||||
neighbourhood.process(voxelMesh, 'cardinal');
|
||||
expect(neighbourhood.hasNeighbour(0, 0, 0, 1, 0, 0)).toBe(true);
|
||||
expect(neighbourhood.getFaceVisibility(0, 0, 0)).toBe(OtS_FaceVisibility.Up | OtS_FaceVisibility.Down | OtS_FaceVisibility.South | OtS_FaceVisibility.East | OtS_FaceVisibility.West);
|
||||
});
|
@ -1,11 +1,8 @@
|
||||
import { AppUtil } from '../src/util';
|
||||
import { ASSERT } from '../src/util/error_util';
|
||||
import { REGEX_NUMBER, REGEX_NZ_ANY, RegExpBuilder } from '../src/util/regex_util';
|
||||
import { TEST_PREAMBLE } from './preamble';
|
||||
|
||||
test('RegExpBuilder', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/hello/)
|
||||
.toRegExp();
|
||||
@ -14,8 +11,6 @@ test('RegExpBuilder', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder REGEX_NUMBER', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const tests = [
|
||||
{ f: '0', s: 0 },
|
||||
{ f: '0.0', s: 0.0 },
|
||||
@ -32,8 +27,6 @@ test('RegExpBuilder REGEX_NUMBER', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder Required-whitespace', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/hello/)
|
||||
.addNonzeroWhitespace()
|
||||
@ -45,8 +38,6 @@ test('RegExpBuilder Required-whitespace', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder Optional', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/hello/)
|
||||
.addNonzeroWhitespace()
|
||||
@ -59,8 +50,6 @@ test('RegExpBuilder Optional', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder Capture', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/[0-9]+/, 'myNumber')
|
||||
.toRegExp();
|
||||
@ -73,8 +62,6 @@ test('RegExpBuilder Capture', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder Capture-multiple', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/[0-9]+/, 'x')
|
||||
.addNonzeroWhitespace()
|
||||
@ -96,8 +83,6 @@ test('RegExpBuilder Capture-multiple', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder Capture-multiple', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/f/)
|
||||
.addNonzeroWhitespace()
|
||||
@ -140,8 +125,6 @@ test('RegExpBuilder Capture-multiple', () => {
|
||||
});
|
||||
|
||||
test('RegExpBuilder Capture-multiple', () => {
|
||||
TEST_PREAMBLE();
|
||||
|
||||
const regex = new RegExpBuilder()
|
||||
.add(/usemtl/)
|
||||
.add(/ /)
|
64
Core/tools/benchmark.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { ObjImporter } from "../src/importers/obj_importer";
|
||||
import { OtS_VoxelMesh_Converter } from '../src/ots_voxel_mesh_converter';
|
||||
import { BlockMesh } from '../src/block_mesh';
|
||||
import { PALETTE_ALL_RELEASE } from '../../Editor/res/palettes/all';
|
||||
import { createReadableStream, createOtSTexture } from './util';
|
||||
import { TexturedMaterial } from 'Core/src/materials';
|
||||
import { ASSERT } from 'Core/src/util/error_util';
|
||||
|
||||
(async () => {
|
||||
const pathModel = path.join(__dirname, '../res/samples/skull.obj');
|
||||
const readableStream = createReadableStream(pathModel);
|
||||
|
||||
console.time('Mesh');
|
||||
const loader = new ObjImporter();
|
||||
const mesh = await loader.import(readableStream);
|
||||
console.timeEnd('Mesh');
|
||||
|
||||
const pathTexture = path.join(__dirname, '../res/samples/skull.jpg');
|
||||
const texture = createOtSTexture(pathTexture);
|
||||
ASSERT(texture !== undefined, `Could not parse ${pathTexture}`);
|
||||
|
||||
// Update the 'skull' material
|
||||
const success = mesh.setMaterial({
|
||||
type: 'textured',
|
||||
name: 'skull',
|
||||
texture: texture,
|
||||
});
|
||||
ASSERT(success, 'Could not update skull material');
|
||||
|
||||
console.time('VoxelMesh');
|
||||
const converter = new OtS_VoxelMesh_Converter();
|
||||
converter.setConfig({
|
||||
constraintAxis: 'y',
|
||||
size: 380,
|
||||
multisampling: false,
|
||||
replaceMode: 'keep',
|
||||
});
|
||||
|
||||
const voxelMesh = converter.process(mesh);
|
||||
console.timeEnd('VoxelMesh');
|
||||
|
||||
|
||||
console.time('BlockMesh');
|
||||
const blockMesh = BlockMesh.createFromVoxelMesh(voxelMesh, {
|
||||
atlasJSON: JSON.parse(fs.readFileSync(path.join(__dirname, '../res/atlases/vanilla.atlas'), 'utf8')),
|
||||
blockPalette: new Set(PALETTE_ALL_RELEASE),
|
||||
calculateLighting: false,
|
||||
contextualAveraging: true,
|
||||
dithering: 'off',
|
||||
ditheringMagnitude: 0,
|
||||
errorWeight: 0.02,
|
||||
resolution: 16,
|
||||
fallable: 'do-nothing',
|
||||
lightThreshold: 0,
|
||||
});
|
||||
console.timeEnd('BlockMesh');
|
||||
|
||||
//console.log(mesh.getTriangleCount().toLocaleString(), 'triangles');
|
||||
//console.log(mesh.getMaterials());
|
||||
//console.log(voxelMesh.getVoxelCount().toLocaleString(), 'voxels');
|
||||
})();
|
@ -2,9 +2,55 @@ import { program } from 'commander';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { getAverageColour, getStandardDeviation } from './misc';
|
||||
|
||||
import { ASSERT } from '../src/util/error_util';
|
||||
import { RGBAUtil } from '../src/colour';
|
||||
import { RGBA } from '../src/colour';
|
||||
|
||||
export function getAverageColour(image: Uint8ClampedArray): RGBA {
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
let a = 0;
|
||||
let weight = 0;
|
||||
for (let x = 0; x < 16; ++x) {
|
||||
for (let y = 0; y < 16; ++y) {
|
||||
const index = 4 * (16 * y + x);
|
||||
const rgba = image.slice(index, index + 4);
|
||||
const alpha = rgba[3] / 255;
|
||||
r += (rgba[0] / 255) * alpha;
|
||||
g += (rgba[1] / 255) * alpha;
|
||||
b += (rgba[2] / 255) * alpha;
|
||||
a += alpha;
|
||||
weight += alpha;
|
||||
}
|
||||
}
|
||||
const numPixels = 16 * 16;
|
||||
return {
|
||||
r: r / weight,
|
||||
g: g / weight,
|
||||
b: b / weight,
|
||||
a: a / numPixels,
|
||||
};
|
||||
}
|
||||
|
||||
export function getStandardDeviation(image: Uint8ClampedArray, average: RGBA): number {
|
||||
let squaredDist = 0.0;
|
||||
let weight = 0.0;
|
||||
for (let x = 0; x < 16; ++x) {
|
||||
for (let y = 0; y < 16; ++y) {
|
||||
const index = 4 * (16 * y + x);
|
||||
const rgba = image.slice(index, index + 4);
|
||||
const alpha = rgba[3] / 255;
|
||||
weight += alpha;
|
||||
const r = (rgba[0] / 255) * alpha;
|
||||
const g = (rgba[1] / 255) * alpha;
|
||||
const b = (rgba[2] / 255) * alpha;
|
||||
squaredDist += Math.pow(r - average.r, 2) + Math.pow(g - average.g, 2) + Math.pow(b - average.b, 2);
|
||||
}
|
||||
}
|
||||
return Math.sqrt(squaredDist / weight);
|
||||
}
|
||||
|
||||
program
|
||||
.argument('<textures_directory>', 'The directory to load the blocks texture files from (assets/minecraft/textures/block)')
|
@ -1,5 +1,4 @@
|
||||
import { PALETTE_ALL_RELEASE } from '../res/palettes/all';
|
||||
import { ColourSpace } from '../src/util';
|
||||
import { PALETTE_ALL_RELEASE } from '../../Editor/res/palettes/all';
|
||||
import { Vector3 } from '../src/vector';
|
||||
import { THeadlessConfig } from './headless';
|
||||
|
||||
@ -10,24 +9,22 @@ export const headlessConfig: THeadlessConfig = {
|
||||
},
|
||||
voxelise: {
|
||||
constraintAxis: 'y',
|
||||
voxeliser: 'bvh-ray',
|
||||
size: 80,
|
||||
useMultisampleColouring: false,
|
||||
voxelOverlapRule: 'average',
|
||||
enableAmbientOcclusion: false, // Only want true if exporting to .obj
|
||||
},
|
||||
assign: {
|
||||
textureAtlas: 'vanilla', // Must be an atlas name that exists in /resources/atlases
|
||||
blockPalette: PALETTE_ALL_RELEASE, // Must be a palette name that exists in /resources/palettes
|
||||
blockPalette: new Set(PALETTE_ALL_RELEASE), // Must be a palette name that exists in /resources/palettes
|
||||
dithering: 'ordered',
|
||||
ditheringMagnitude: 32,
|
||||
colourSpace: ColourSpace.RGB,
|
||||
fallable: 'replace-falling',
|
||||
resolution: 32,
|
||||
calculateLighting: false,
|
||||
lightThreshold: 0,
|
||||
contextualAveraging: true,
|
||||
errorWeight: 0.0,
|
||||
atlasJSON: undefined,
|
||||
},
|
||||
export: {
|
||||
exporter: 'litematic', // 'schematic' / 'litematic',
|
@ -1,7 +1,7 @@
|
||||
import { StatusHandler } from '../src/status';
|
||||
import { StatusHandler } from '../../Editor/src/status';
|
||||
import { LOG_MAJOR, Logger, TIME_END, TIME_START } from '../src/util/log_util';
|
||||
import { WorkerClient } from '../src/worker_client';
|
||||
import { AssignParams, ExportParams, ImportParams, VoxeliseParams } from '../src/worker_types';
|
||||
import { WorkerClient } from '../../Editor/src/worker/worker_client';
|
||||
import { AssignParams, ExportParams, ImportParams, VoxeliseParams } from '../../Editor/src/worker/worker_types';
|
||||
|
||||
export type THeadlessConfig = {
|
||||
import: ImportParams.Input,
|
16
Core/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [ "es2022", "DOM" ],
|
||||
"module": "commonjs",
|
||||
"target": "es2022",
|
||||
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*", "index.ts"]
|
||||
}
|
@ -30,7 +30,6 @@ export const en_GB = {
|
||||
no_vertices_loaded: 'No vertices were loaded',
|
||||
no_triangles_loaded: 'No triangles were loaded',
|
||||
could_not_scale_mesh: 'Could not scale mesh correctly - mesh is likely 2D, rotate it so that it has a non-zero height',
|
||||
invalid_encoding: 'Unrecognised character found, please encode using UTF-8',
|
||||
invalid_face_data: 'Face data has unexpected number of vertex data: {{count, number}}',
|
||||
too_many_triangles: 'The imported mesh has {{count, number}} triangles, consider simplifying it in a DDC such as Blender',
|
||||
vertex_triangle_count: '{{vertex_count, number}} vertices, {{triangle_count, number}} triangles',
|
||||
@ -38,6 +37,11 @@ export const en_GB = {
|
||||
failed_to_parse_line: 'Failed attempt to parse "{{line}}", because "{{error}}"',
|
||||
gltf_experimental: 'The GLTF importer is experimental and may produce unexpected results',
|
||||
unsupported_image_type: 'Cannot read \'{{file_name}}\', unsupported file type \'{{file_type}}\'',
|
||||
invalid_data: 'Invalid data',
|
||||
invalid_encoding: 'Unsupported encoding, please use UTF8',
|
||||
invalid_material_name: 'Invalid material name',
|
||||
invalid_image_format: 'Invalid image format',
|
||||
could_not_parse: 'Failed to load model file',
|
||||
components: {
|
||||
input: '3D Model (.obj, .glb)',
|
||||
no_file_chosen: 'No file chosen',
|
||||
@ -97,6 +101,7 @@ export const en_GB = {
|
||||
ncrb: 'NCRB',
|
||||
average_recommended: 'Average (recommended)',
|
||||
first: 'First',
|
||||
repalce: 'Replace',
|
||||
on_recommended: 'On (recommended)',
|
||||
off_faster: 'Off (faster)',
|
||||
},
|
9696
Editor/package-lock.json
generated
Normal file
39
Editor/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "ots-editor",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve --config ./webpack.dev.js",
|
||||
"build": "webpack --config ./webpack.prod.js",
|
||||
"test": "echo \"No tests\" && exit 0"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ots-core": "file:../Core"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pngjs": "^6.0.2",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"ga-gtag": "^1.1.7",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"i18next": "^23.5.1",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"jszip": "^3.10.1",
|
||||
"node-polyfill-webpack-plugin": "^2.0.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"split.js": "^1.6.5",
|
||||
"ts-loader": "^9.5.0",
|
||||
"twgl.js": "^5.5.3",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-merge": "^5.9.0",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://objtoschematic.com/" />
|
||||
<meta property="og:image" content="https://i.imgur.com/TwdIpyb.png" />
|
||||
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 309 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 5.2 MiB |
Before Width: | Height: | Size: 537 KiB After Width: | Height: | Size: 537 KiB |