Compare commits

...

73 Commits

Author SHA1 Message Date
Lucas Dower
7d3cce1c81 Fixed incorrect litematic exporter stride, fixes #153 2023-11-04 20:18:06 +00:00
Lucas Dower
ac1505fa0d Fixed incorrect pako compression function 2023-11-04 19:27:19 +00:00
Lucas Dower
a1d0fa1aff Removed unused linear_allocator.ts
* Updated misc types
2023-11-04 12:27:27 +00:00
Lucas Dower
1d051dbcc8 Reimplement colour resolution to OtS_BlockMesh_Converter 2023-10-26 19:04:54 +01:00
Lucas Dower
30848dc02a Fixed typo resulting in z-plane not being rasterised 2023-10-26 18:32:03 +01:00
Lucas Dower
7be3b89159 Optimised voxelisation, ~2x improvement 2023-10-24 20:06:31 +01:00
Lucas Dower
6e81858e35 Optimised voxelisation 2023-10-22 17:57:44 +01:00
Lucas Dower
70d4df2d2d Replace RGBAColours with OtS_Colours 2023-10-22 17:39:19 +01:00
Lucas Dower
0f543d809d Optimised OtS_VoxelMesh_Converter 2023-10-21 12:43:53 +01:00
Lucas Dower
43962b1719
Update README.md 2023-10-20 01:59:54 +01:00
Lucas Dower
7b61bff346 Updated OtS_BlockMeshConverter, refactored contextual averaging 2023-10-15 17:03:43 +01:00
Lucas Dower
0766f628a0 Updated Editor block mesh render buffer for new OtS_BlockMesh 2023-10-14 01:14:33 +01:00
Lucas Dower
2754ecf3f6 Added OtS_BlockMesh and updated exporters to use 2023-10-14 00:50:57 +01:00
Lucas Dower
c51c4d900c Fixed getSections not returning a copy of the section, fixed copy on mesh 2023-10-14 00:06:45 +01:00
Lucas Dower
3729986623 Fixed _sampleNearest flooring values 2023-10-14 00:02:26 +01:00
Lucas Dower
803cc6c326 Fixed incorrect getTriangles, fixed _sampleNearest and _sampleLinear being swapped 2023-10-13 23:40:11 +01:00
Lucas Dower
b7d6bce1a2 Updated Editor mesh rendering 2023-10-13 22:29:18 +01:00
Lucas Dower
61b32b2b0d Refactored mesh into mesh sections 2023-10-12 00:59:50 +01:00
Lucas Dower
8232fe8098 Fix choice of importer and update webpack build directory name 2023-10-10 22:31:59 +01:00
Lucas Dower
926862c7a9 Moved DeepPartial type from Core to Editor 2023-10-10 22:31:03 +01:00
Lucas Dower
6e5de859f9 Temporarily update Editor build workflow 2023-10-10 21:16:47 +01:00
Lucas Dower
243595c39d Fix Editor npm dependencies 2023-10-10 20:59:30 +01:00
Lucas Dower
16cc8290c9 Update and rename helper scripts 2023-10-10 20:58:45 +01:00
Lucas Dower
87facde6eb Add helper scripts for building and testing all packages 2023-10-10 20:01:55 +01:00
Lucas Dower
06a915d3b7 Added Sandbox package for API examples 2023-10-10 19:39:13 +01:00
Lucas Dower
a7838305ca Removed Atlas, moved occlusion from Core to Editor 2023-10-06 21:43:03 +01:00
Lucas Dower
00389813bf Refactored Triangle 2023-10-06 21:11:44 +01:00
Lucas Dower
2fb092f835 Added /examples/ directory 2023-10-06 20:51:47 +01:00
Lucas Dower
b72de9b439
Update README.md 2023-10-06 18:40:18 +01:00
Lucas Dower
168874c091 Removed old files 2023-10-06 18:36:31 +01:00
Lucas Dower
255f9fb147 Fixed Core dependencies 2023-10-06 18:32:47 +01:00
Lucas Dower
17e385b637 Reorganise modules into two discrete packages 2023-10-06 18:08:36 +01:00
Lucas Dower
845b8a15a2 Fixes for editor material changes 2023-09-22 21:18:48 +01:00
Lucas Dower
653bd14b9e Nuked disastrous Texture class for OtS_Texture and OtSE_TextureReader 2023-09-21 20:49:51 +01:00
Lucas Dower
4d77a58d17 Change MaterialType enum to OtS_MaterialType type 2023-09-21 17:50:36 +01:00
Lucas Dower
9fae5a4336 Rewrite Mesh into new OtS_Mesh 2023-09-15 19:44:19 +01:00
Lucas Dower
bc1ee90b92 Removed unused dependencies, minor importer refactor 2023-09-15 19:44:05 +01:00
Lucas Dower
5aca2d2cda Convert UV from class to type 2023-09-15 19:43:48 +01:00
Lucas Dower
c6da004f6e Minor texture sampling optimisation 2023-09-15 02:10:30 +01:00
Lucas Dower
48e884c4e6 Optimised allocations in hot voxeliser code 2023-09-15 01:45:17 +01:00
Lucas Dower
d5ff00f834 Added OtS_VoxelMesh_Converter, nuked legacy voxelisers 2023-09-15 00:55:53 +01:00
Lucas Dower
882d8a476c Removed new-palette-blocks.txt 2023-09-15 00:12:44 +01:00
Lucas Dower
9bb3e4fe9f Removed misc.ts, moved functions into build-atlas.ts 2023-09-14 01:20:59 +01:00
Lucas Dower
dfe848381c Removed palette.ts 2023-09-14 01:17:48 +01:00
Lucas Dower
4cf0a64cb1 Remove colour space 2023-09-14 00:58:18 +01:00
Lucas Dower
544a2433e5 Removed 'static' folder in /res/ 2023-09-14 00:51:19 +01:00
Lucas Dower
fbbc57ad52 Cleaned up template.html and styles.css and webpack config 2023-09-14 00:47:32 +01:00
Lucas Dower
f533f84fc4 Simplified tsconfig.json 2023-09-14 00:40:16 +01:00
Lucas Dower
8d2edcbc99
Removed Renderer dependency in ShaderManager 2023-09-08 22:06:15 +01:00
Lucas Dower
8df204e914
Removed legacy hash_map.ts 2023-09-08 22:00:17 +01:00
Lucas Dower
6856cab23e
Removed Renderer dependency in MouseManager 2023-09-08 21:58:14 +01:00
Lucas Dower
df2ea5b5b5
Minor updates to .gitignore 2023-09-08 21:54:12 +01:00
Lucas Dower
9a20c95391
Removed simple circular dependency chain 2023-09-08 21:51:35 +01:00
Lucas Dower
35241260f2
Moved OtS_VoxelMesh_Neighbourhood to separate module, renamed EFaceVisibility to OtS_FaceVisibility 2023-09-08 21:36:52 +01:00
Lucas Dower
a2da8d4aa2
Added tests for OtS_VoxelMesh_Neighbourhood 2023-09-08 21:28:10 +01:00
Lucas Dower
73552e8ec0
Fixed OtS_VoxelMesh bugs, added respective tests 2023-09-08 20:45:47 +01:00
Lucas Dower
58ca25db18
Added some OtS_VoxelMesh tests 2023-09-08 20:08:01 +01:00
Lucas Dower
f2f4ee94a4
Nuked VoxelMesh, replaced with rewritten OtS_VoxelMesh 2023-09-08 19:42:48 +01:00
Lucas Dower
d4f2934dcf
Initial commit for VoxelMesh rewrite 2023-09-08 19:05:29 +01:00
Lucas Dower
0ea18700c5
Remove webpack asset dependency in runtime module 2023-09-07 23:51:50 +01:00
Lucas Dower
4e8f3680e5
Finished up initial round of runtime editor dependency split 2023-09-07 23:31:10 +01:00
Lucas Dower
66d9619b9b
Removed various status handling to future rework 2023-09-07 23:22:26 +01:00
Lucas Dower
ec3743a4c9
Moved a bunch of rendering modules into a rendering editor directory 2023-09-07 21:16:56 +01:00
Lucas Dower
2c3cee5c4a
Removed status.ts editor dependency from runtime module 2023-09-07 21:07:23 +01:00
Lucas Dower
72cf3dc53b
Refactored a bunch of importer code, still a bit rough 2023-09-07 20:42:18 +01:00
Lucas Dower
63d982b148
Moved editor buffer generation out of various runtime classes 2023-09-07 18:55:29 +01:00
Lucas Dower
4aaf0a7f9d
Moved buffer.ts from runtime to editor 2023-09-07 00:04:52 +01:00
Lucas Dower
60cd73a455
Removed config.ts editor dependency in runtime file 2023-09-07 00:01:34 +01:00
Lucas Dower
fb64ba342f
Removed worker_types.ts dependency in runtime file 2023-09-06 23:55:12 +01:00
Lucas Dower
1864e5d446
Removed status.ts editor dependency in runtime file 2023-09-06 23:40:48 +01:00
Lucas Dower
f66d9069f1
Removed progress.ts editor dependency in runtime file 2023-09-06 23:31:57 +01:00
Lucas Dower
b5c228154c
Moved status.ts to editor 2023-09-06 23:00:03 +01:00
Lucas Dower
33ce259926
Major reshuffle of files into separate editor and runtime directories 2023-09-06 22:58:18 +01:00
215 changed files with 26973 additions and 26344 deletions

View File

@ -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
View 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
View 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

View File

@ -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
View 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
View 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
View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

27
Core/package.json Normal file
View 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"
}
}

View File

@ -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,
};
}
}

View File

@ -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
}

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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(),
);
}
}

View File

@ -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 };
}
}

View File

@ -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() {
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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 };
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View 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 };
}
}

View 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>;
}

View File

@ -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;
}
}

View 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();
}
}
}

View 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;
}
}

View File

@ -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
View 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);
// }
// }
// }

View File

@ -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
View 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;
}
}

View 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
View 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
View 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
View 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
View 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 };
}
},
};
}
}

View 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),
}
}
}
}

View 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
View 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
View 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),
),
);
}
}

View File

@ -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);
}

View 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;
}

View File

@ -0,0 +1,6 @@
export function ASSERT(condition: any, errorMessage: string = 'Assertion Failed'): asserts condition {
if (!condition) {
Error(errorMessage);
throw Error(errorMessage);
}
}

View File

@ -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.

View File

@ -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);
}

View 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 } };

View File

@ -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);
};

View 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');
});

View 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);
});

View 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);
});

View 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);
});

View File

@ -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
View 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');
})();

View File

@ -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)')

View File

@ -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',

View File

@ -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
View 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"]
}

View File

@ -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

File diff suppressed because it is too large Load Diff

39
Editor/package.json Normal file
View 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"
}
}

View File

@ -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">

View File

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 309 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 5.2 MiB

View File

Before

Width:  |  Height:  |  Size: 537 KiB

After

Width:  |  Height:  |  Size: 537 KiB

Some files were not shown because too many files have changed in this diff Show More