Skip to content

Commit f867f42

Browse files
committed
Block textures
1 parent b505871 commit f867f42

14 files changed

Lines changed: 588 additions & 210 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
"dependencies": {
1515
"@gltf-transform/core": "^4.3.0",
1616
"@gltf-transform/functions": "^4.3.0",
17+
"layout": "^2.2.0",
1718
"node-stream-zip": "^1.15.0",
1819
"prismarine-nbt": "^2.8.0",
19-
"quick-front": "^1.3.0"
20+
"sharp": "^0.34.5"
2021
}
2122
}

src/index.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { readFile, writeFile } from "node:fs/promises"
44
import { basename, dirname, extname, join } from "node:path"
55
import { Cli } from "./cli/Cli"
66
import { Type } from "./struct/Type"
7+
import { TextureAtlas } from "./structureExporter/AtlasManager"
8+
import { BlockBuilder } from "./structureExporter/BlockBuilder"
79
import { CompositeBuilder } from "./structureExporter/CompositeBuilder"
810
import { info } from "./structureExporter/log"
911
import { ModelManager } from "./structureExporter/ModelManager"
@@ -20,8 +22,10 @@ const cli = new Cli("structureExporter")
2022
options: {
2123
cache: Type.string.as(Type.nullable),
2224
simplify: Type.boolean.as(Type.nullable),
25+
dryRun: Type.boolean.as(Type.nullable),
26+
dumpAtlas: Type.boolean.as(Type.nullable),
2327
},
24-
async callback(input, output, { cache, simplify }) {
28+
async callback(input, output, { cache, simplify, dryRun, dumpAtlas }) {
2529
const sources = await SourceManager.createOrOpen(cache)
2630

2731
if (output == null) {
@@ -36,9 +40,20 @@ const cli = new Cli("structureExporter")
3640
const document = new Document()
3741
const scene = document.createScene()
3842

39-
const modelManager = new ModelManager(document, sources)
43+
const modelManager = new ModelManager(sources)
4044

41-
await new CompositeBuilder(document, modelManager, scene).addStructure(structure)
45+
await modelManager.prepareAssets(structure.palette)
46+
47+
const atlas = await TextureAtlas.build(document, modelManager)
48+
if (dumpAtlas) {
49+
await writeFile(join(dirname(output), "atlas.png"), atlas.content)
50+
}
51+
52+
if (dryRun) return
53+
54+
const blockBuilder = new BlockBuilder(document, modelManager, atlas)
55+
56+
new CompositeBuilder(document, blockBuilder, scene).addStructure(structure)
4257

4358
if (simplify) {
4459
await document.transform(
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Document, Material, Texture, TextureInfo } from "@gltf-transform/core"
2+
import { ModelManager } from "./ModelManager"
3+
// @ts-ignore
4+
import layout from "layout"
5+
import sharp from "sharp"
6+
import { unreachable } from "../comTypes/util"
7+
import { TextureResource } from "./TextureResource"
8+
9+
interface AtlasLayout<T> {
10+
height: number
11+
width: number
12+
items: {
13+
x: number
14+
y: number
15+
height: number
16+
width: number
17+
meta: T
18+
}[]
19+
}
20+
21+
22+
export class TextureAtlas {
23+
protected _createTextureMaterial(name: string) {
24+
const material = this.document.createMaterial(name)
25+
.setBaseColorTexture(this.texture)
26+
.setMetallicFactor(0)
27+
.setRoughnessFactor(1)
28+
29+
const info = material.getBaseColorTextureInfo() ?? unreachable()
30+
info.setMinFilter(TextureInfo.MinFilter.NEAREST)
31+
info.setMagFilter(TextureInfo.MagFilter.NEAREST)
32+
33+
return material
34+
}
35+
36+
protected _opaqueMaterial: Material | null = null
37+
public getOpaqueMaterial() {
38+
return this._opaqueMaterial ??= this._createTextureMaterial("atlas_opaque")
39+
.setAlphaMode("OPAQUE")
40+
}
41+
42+
public getUVs(texture: TextureResource, spec: readonly [number, number, number, number]) {
43+
let [x1, y1, x2, y2] = spec
44+
x1 = (x1 + texture.x) / this.width
45+
x2 = (x2 + texture.x) / this.width
46+
y1 = (y1 + texture.y) / this.height
47+
y2 = (y2 + texture.y) / this.height
48+
49+
return [
50+
x1, y2,
51+
x2, y2,
52+
x1, y1,
53+
x2, y1,
54+
]
55+
}
56+
57+
protected constructor(
58+
public readonly textures: TextureResource[],
59+
public readonly document: Document,
60+
public readonly width: number,
61+
public readonly height: number,
62+
public readonly texture: Texture,
63+
public readonly content: Buffer,
64+
) { }
65+
66+
public static async build(document: Document, models: ModelManager) {
67+
const textures = [...new Set(models.listUsedTextures())]
68+
const atlasBuilder = layout("binary-tree")
69+
70+
for (const texture of textures) {
71+
atlasBuilder.addItem({ width: texture.width, height: texture.height, meta: texture })
72+
}
73+
74+
const atlasLayout: AtlasLayout<TextureResource> = atlasBuilder.export()
75+
const atlas = sharp({
76+
create: {
77+
width: atlasLayout.width,
78+
height: atlasLayout.height,
79+
background: { r: 0, g: 0, b: 0, alpha: 0 },
80+
channels: 4,
81+
},
82+
}).png()
83+
84+
atlas.composite(await Promise.all(atlasLayout.items.map(async item => {
85+
const texture = item.meta
86+
87+
texture.x = item.x
88+
texture.y = item.y
89+
90+
return {
91+
input: await texture.image.toBuffer(),
92+
left: item.x,
93+
top: item.y,
94+
}
95+
})))
96+
97+
const atlasData = await atlas.toBuffer()
98+
const texture = document
99+
.createTexture("atlas")
100+
.setImage(atlasData)
101+
.setMimeType("image/png")
102+
103+
return new TextureAtlas(textures, document, atlasLayout.width, atlasLayout.height, texture, atlasData)
104+
}
105+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Document, Mesh, Node } from "@gltf-transform/core"
2+
import { compactPrimitive } from "@gltf-transform/functions"
3+
import { unreachable } from "../comTypes/util"
4+
import { TextureAtlas } from "./AtlasManager"
5+
import { BlockModel } from "./BlockModel"
6+
import { BlockState } from "./BlockState"
7+
import { FaceInfo } from "./FaceInfo"
8+
import { FACE_DOWN, FACE_EAST, FACE_NORTH, FACE_SOUTH, FACE_UP, FACE_WEST } from "./FACES"
9+
import { warn } from "./log"
10+
import { ModelManager } from "./ModelManager"
11+
12+
const _FACE_DATA = [
13+
// Vertex data: 4 vertices, each has 3 components
14+
// 2---3
15+
// | |
16+
// 0---1
17+
[FACE_SOUTH, [-0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5]],
18+
[FACE_NORTH, [0.5, -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5]],
19+
[FACE_WEST, [-0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5]],
20+
[FACE_EAST, [0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5]],
21+
[FACE_UP, [-0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5]],
22+
[FACE_DOWN, [-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5]],
23+
] as const
24+
25+
export class BlockBuilder {
26+
protected _meshCache = new Map<string, Mesh>()
27+
protected _buffer = this.document.createBuffer()
28+
29+
public getBlockMesh(model: BlockModel, elementIdx: number, faceMask: number, faces: (FaceInfo | null)[]) {
30+
const key = `${model.name}_${elementIdx}_${faceMask}`
31+
const existing = this._meshCache.get(key)
32+
if (existing) return existing
33+
34+
const vertexValues: number[] = []
35+
const indexValues: number[] = []
36+
const uvValues: number[] = []
37+
38+
let indexStart = 0
39+
40+
for (const [face, vertices] of _FACE_DATA) {
41+
if ((faceMask & face) == 0) continue
42+
43+
indexValues.push(indexStart + 0, indexStart + 1, indexStart + 2, indexStart + 1, indexStart + 3, indexStart + 2)
44+
45+
// Increment index start by 4 because we will add 4 vertices
46+
indexStart += 4
47+
48+
vertexValues.push(...vertices)
49+
50+
const index = 31 - Math.clz32(face)
51+
const faceInfo = faces[index] ?? unreachable()
52+
const texture = model.resolveTexture(faceInfo.texture)
53+
uvValues.push(...this.atlas.getUVs(texture, faceInfo.uv))
54+
}
55+
56+
const vertices = this.document.createAccessor()
57+
.setType("VEC3")
58+
.setArray(new Float32Array(vertexValues))
59+
.setBuffer(this._buffer)
60+
61+
const uvs = this.document.createAccessor()
62+
.setType("VEC2")
63+
.setArray(new Float32Array(uvValues))
64+
.setBuffer(this._buffer)
65+
66+
const indices = this.document.createAccessor()
67+
.setArray(new Uint16Array(indexValues))
68+
.setBuffer(this._buffer)
69+
70+
const prim = this.document.createPrimitive()
71+
.setAttribute("POSITION", vertices)
72+
.setAttribute("TEXCOORD_0", uvs)
73+
.setIndices(indices)
74+
.setMaterial(this.atlas.getOpaqueMaterial())
75+
76+
compactPrimitive(prim)
77+
78+
const mesh = this.document.createMesh(key)
79+
.addPrimitive(prim)
80+
81+
this._meshCache.set(key, mesh)
82+
return mesh
83+
}
84+
85+
public buildBlockState(state: BlockState, node: Node) {
86+
const possibleStates = this.models.getBlockModels(state.block)
87+
if (possibleStates == null) {
88+
// We already warned about this in ModelManager.prepareAssets
89+
return
90+
}
91+
92+
if (possibleStates.multipart) {
93+
let j = 0
94+
95+
for (const part of possibleStates.findModels(state)) {
96+
const partNode = this.document.createNode(`part_${j++}`)
97+
node.addChild(partNode)
98+
99+
for (let i = 0; i < part.elements.length; i++) {
100+
const child = this.document.createNode(`part_${j}_element_${i}`)
101+
part.elements[i].apply(child, part, this.document, this)
102+
partNode.addChild(child)
103+
}
104+
105+
if (part.rotation) {
106+
partNode.setRotation(part.rotation)
107+
}
108+
}
109+
} else {
110+
const model = possibleStates.findModel(state)
111+
112+
if (model == null) {
113+
warn(`Failed to find matching model for block state ${state}`)
114+
return
115+
}
116+
117+
if (model.elements.length == 0) return
118+
119+
for (let i = 0; i < model.elements.length; i++) {
120+
const child = this.document.createNode(`element_${i}`)
121+
model.elements[i].apply(child, model, this.document, this)
122+
node.addChild(child)
123+
}
124+
125+
if (model.rotation) {
126+
node.setRotation(model.rotation)
127+
}
128+
}
129+
}
130+
131+
constructor(
132+
public readonly document: Document,
133+
public readonly models: ModelManager,
134+
public readonly atlas: TextureAtlas,
135+
) { }
136+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { vec4 } from "@gltf-transform/core"
2+
import { BlockModelElement } from "./BlockModelElement"
3+
import { BlockState } from "./BlockState"
4+
import { TextureResource } from "./TextureResource"
5+
import { Vector3 } from "./Vector3"
6+
import { warn } from "./log"
7+
8+
9+
export class BlockModel {
10+
protected readonly _textureVariables = new Map<string, TextureResource | string>()
11+
12+
public withRotation(rotation: Vector3) {
13+
return new BlockModel(this.name, this.elements, rotation.mul1(Math.PI / 180).eulerToQuaternionZYX())
14+
.copyTextureVariables(this)
15+
}
16+
17+
public setTextureVariable(key: string, value: TextureResource | string) {
18+
this._textureVariables.set(key, value)
19+
return this
20+
}
21+
22+
public getTextureVariable(key: string) {
23+
let value
24+
while (true) {
25+
value = this._textureVariables.get(key)
26+
if (value == null) return null
27+
if (typeof value != "string") return value
28+
key = value
29+
}
30+
}
31+
32+
public resolveTexture(texture: TextureResource | string) {
33+
if (typeof texture != "string") return texture
34+
35+
const resolvedTexture = this.getTextureVariable(texture)
36+
37+
if (resolvedTexture == null) {
38+
warn(`Cannot resolve texture variable #${texture} in model ${this.name}`)
39+
return TextureResource.getFallback()
40+
}
41+
42+
return resolvedTexture
43+
}
44+
45+
public copyTextureVariables(from: BlockModel) {
46+
for (const [key, value] of from._textureVariables) {
47+
this.setTextureVariable(key, value)
48+
}
49+
return this
50+
}
51+
52+
constructor(
53+
public readonly name: string,
54+
public readonly elements: BlockModelElement[],
55+
public readonly rotation: vec4 | null,
56+
) { }
57+
}
58+
59+
export class BlockModelRouter {
60+
protected readonly _states: [BlockState, BlockModel][] = []
61+
62+
public registerModel(state: BlockState, model: BlockModel) {
63+
this._states.push([state, model])
64+
}
65+
66+
public findModel(target: BlockState) {
67+
for (const [state, model] of this._states) {
68+
if (state.isSubsetOf(target)) {
69+
return model
70+
}
71+
}
72+
73+
return null
74+
}
75+
76+
public *findModels(target: BlockState) {
77+
for (const [state, model] of this._states) {
78+
if (state.isSubsetOf(target)) {
79+
yield model
80+
}
81+
}
82+
}
83+
84+
constructor(
85+
public readonly multipart: boolean,
86+
) { }
87+
}

0 commit comments

Comments
 (0)