"use strict";
/**
* @typedef {Object} Model
* @property {WebGLBuffer} position
* @property {WebGLBuffer} indices
* @property {WebGLBuffer} texture
* @property {WebGLBuffer} lighting - Prebaked lighting
* @property {number} vertexCount
* @property {number} scalePadded - Ortho matrix scaling when addPadding=true
* @property {number} scaleUnpadded - Ortho matrix scaling when addPadding=false
* @property {boolean} is3D - Whether to apply 30° rotation for isometric view
*/
/**
* One of {@link MODEL_CUBE}, {@link MODEL_ITEM}, {@link MODEL_SKULL_COMPACT}, {@link MODEL_SKULL_SKIN}.
* @public
* @hideconstructor
*/
export class ModelType {
constructor(generator) {
this.generator = generator;
}
/**
* @param {WebGLRenderingContext} gl
* @returns {Model}
*/
create(gl) {
return this.generator(gl);
}
}
// Extracted from https://mcasset.cloud/1.8.9/assets/minecraft/textures/misc/enchanted_item_glint.png
// First row, only R component
const GLINT_TEXTURE_RAW = [
0x68, 0x31, 0x5B, 0x38, 0x4F, 0x62, 0x6D, 0x59, 0x56, 0x71, 0x6B, 0x84, 0x82, 0xA0, 0x6C, 0x6A,
0x74, 0x6D, 0xA3, 0xDD, 0xB8, 0xA0, 0x95, 0x83, 0x86, 0x86, 0x3F, 0x18, 0x41, 0x4E, 0x5E, 0x7D,
0x79, 0x79, 0x83, 0x5D, 0x7E, 0x8F, 0x90, 0xA6, 0x9B, 0xBD, 0xCC, 0xFF, 0xE5, 0xDA, 0xD1, 0xC5,
0x9A, 0x69, 0x68, 0x9C, 0x8B, 0x89, 0x57, 0x50, 0x6F, 0x57, 0x5F, 0x67, 0x6C, 0x94, 0x98, 0x4B
];
// Uncompressed texture
export const GLINT_TEXTURE = new ImageData(new Uint8ClampedArray(new Array(64).fill(GLINT_TEXTURE_RAW.map(c => [
// Minecraft uses a color multiplier of 0x8040CC when rendering the enchantment texture.
// We bake it directly into the texture for simplicity.
c * 0x80 / 0xFF,
c * 0x40 / 0xFF,
c * 0xCC / 0xFF,
0xFF,
])).flat(2)), 64, 64);
/**
* Simple 2D plane / Minecraft item.
* @type {ModelType}
* @public
*/
export const MODEL_ITEM = new ModelType(gl => {
const position = positionsBuf(gl, positions(1, [
[1, 1, 1], [0, 1, 1], [0, 0, 1], [1, 0, 1],
]));
const indices = indicesBuf(gl, 1);
const texture = textureBuf(gl, 1, [
[0, 0]
]);
const lighting = lightingBuf(gl, [
255
]);
return {
position,
indices,
texture,
lighting,
vertexCount: 6 * 1,
scalePadded: 1.0,
scaleUnpadded: 1.0,
is3D: false
};
});
/**
* Simple cube / Minecraft block.
* @type {ModelType}
* @public
*/
export const MODEL_CUBE = new ModelType(gl => {
// Removed culled faces
const position = positionsBuf(gl, positions(1, [
[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0], // Top
[0, 1, 1], [0, 1, 0], [0, 0, 0], [0, 0, 1], // Left
[1, 1, 1], [0, 1, 1], [0, 0, 1], [1, 0, 1], // Right
]));
const indices = indicesBuf(gl, 3);
const texture = textureBuf(gl, 2, [
[0, 0], // Top
[0, 1], // Left
[1, 0], // Right
]);
const lighting = lightingBuf(gl, [
255, // Top
162, // Left
111, // Right
]);
return {
position,
indices,
texture,
lighting,
vertexCount: 6 * 3,
scalePadded: 1.6,
scaleUnpadded: 1.5732,
is3D: true
};
});
/**
* Skeleton / Player skull, using the Minecraft skin texture layout.
* @type {ModelType}
* @public
*/
export const MODEL_SKULL_SKIN = new ModelType(gl => {
// Removed culled faces
const position = positionsBuf(gl, [
positions(8 / 16, [
[1, 1, 0], [0, 1, 0], [0, 1, 1], [1, 1, 1], // Lower, Top
[0, 1, 1], [0, 1, 0], [0, 0, 0], [0, 0, 1], // Lower, Left
[1, 1, 1], [0, 1, 1], [0, 0, 1], [1, 0, 1], // Lower, Right
]),
positions(8.5 / 16, [
[1, 1, 0], [0, 1, 0], [0, 1, 1], [1, 1, 1], // Upper, Top
[1, 0, 0], [0, 0, 0], [0, 0, 1], [1, 0, 1], // Upper, Bottom
[0, 1, 1], [0, 1, 0], [0, 0, 0], [0, 0, 1], // Upper, Left
[1, 1, 0], [1, 1, 1], [1, 0, 1], [1, 0, 0], // Upper, Back Right
[1, 1, 1], [0, 1, 1], [0, 0, 1], [1, 0, 1], // Upper, Right
[0, 1, 0], [1, 1, 0], [1, 0, 0], [0, 0, 0], // Upper, Back Left
])
]);
const indices = indicesBuf(gl, 9);
const texture = textureBuf(gl, 8, [
[1, 0], // Lower, Top
[0, 1], // Lower, Left
[1, 1], // Lower, Right
[5, 0], // Upper, Top
[6, 0], // Upper, Bottom
[4, 1], // Upper, Left
[6, 1], // Upper, Back Right
[5, 1], // Upper, Right
[7, 1], // Upper, Back Left
]);
const lighting = lightingBuf(gl, [
255, // Lower, Top
162, // Lower, Left
111, // Lower, Right
255, // Upper, Top
102, // Upper, Bottom
162, // Upper, Left
190, // Upper, Back Right
111, // Upper, Right
184, // Upper, Back Left
]);
return {
position,
indices,
texture,
lighting,
vertexCount: 6 * 9,
scalePadded: 1.12,
scaleUnpadded: 0.836,
is3D: true
};
});
/**
* Skeleton / Player skull, using a compact texture layout.
* @type {ModelType}
* @public
*/
export const MODEL_SKULL_COMPACT = new ModelType(gl => {
// Removed culled faces
const position = positionsBuf(gl, [
positions(8 / 16, [
[1, 1, 0], [0, 1, 0], [0, 1, 1], [1, 1, 1], // Lower, Top
[0, 1, 1], [0, 1, 0], [0, 0, 0], [0, 0, 1], // Lower, Left
[1, 1, 1], [0, 1, 1], [0, 0, 1], [1, 0, 1], // Lower, Right
]),
positions(8.5 / 16, [
[1, 1, 0], [0, 1, 0], [0, 1, 1], [1, 1, 1], // Upper, Top
[1, 0, 0], [0, 0, 0], [0, 0, 1], [1, 0, 1], // Upper, Bottom
[0, 1, 1], [0, 1, 0], [0, 0, 0], [0, 0, 1], // Upper, Left
[1, 1, 0], [1, 1, 1], [1, 0, 1], [1, 0, 0], // Upper, Back Right
[1, 1, 1], [0, 1, 1], [0, 0, 1], [1, 0, 1], // Upper, Right
[0, 1, 0], [1, 1, 0], [1, 0, 0], [0, 0, 0], // Upper, Back Left
])
]);
const indices = indicesBuf(gl, 9);
const texture = textureBuf(gl, 3, [
[0, 0], // Lower, Top
[0, 1], // Lower, Left
[1, 0], // Lower, Right
[2, 0], // Upper, Top
[0, 2], // Upper, Bottom
[1, 1], // Upper, Left
[1, 2], // Upper, Back Right
[2, 1], // Upper, Right
[2, 2], // Upper, Back Left
]);
const lighting = lightingBuf(gl, [
255, // Lower, Top
162, // Lower, Left
111, // Lower, Right
255, // Upper, Top
102, // Upper, Bottom
162, // Upper, Left
190, // Upper, Back Right
111, // Upper, Right
184, // Upper, Back Left
]);
return {
position,
indices,
texture,
lighting,
vertexCount: 6 * 9,
scalePadded: 1.12,
scaleUnpadded: 0.836,
is3D: true
};
});
// Helper functions
/**
* @param {number} scale
* @param {number[][]} positions - For each rectangle 4 arrays with 3 entries of 0 or 1 - just look at the usages idk what to write
* @returns {number[]}
*/
function positions(scale, positions) {
return positions
.flat()
.map(v => ((v * 2) - 1) * scale);
}
/**
* @param {WebGLRenderingContext} gl
* @param {GLenum} target
* @param {AllowSharedBufferSource} data
* @returns {WebGLBuffer}
*/
function buf(gl, target, data) {
const buffer = gl.createBuffer();
gl.bindBuffer(target, buffer);
gl.bufferData(target, data, gl.STATIC_DRAW);
return buffer;
}
/**
* @param {WebGLRenderingContext} gl
* @param {number[]} data - see positions
* @returns {WebGLBuffer}
*/
function positionsBuf(gl, data) {
return buf(gl, gl.ARRAY_BUFFER, new Float32Array(data.flat()));
}
/**
* @param {WebGLRenderingContext} gl
* @param {number} rectangleCount
* @returns {WebGLBuffer}
*/
function indicesBuf(gl, rectangleCount) {
let vertexData = [...Array(rectangleCount).keys()]
.map(i => ([i * 4, i * 4 + 1, i * 4 + 2, i * 4, i * 4 + 2, i * 4 + 3]))
.flat();
return buf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexData));
}
/**
* @param {WebGLRenderingContext} gl
* @param {number} units - How many units the texture is split into, per axis
* @param {number[][]} data - For each rect [u, v] in units
* @returns {WebGLBuffer}
*/
function textureBuf(gl, units, data) {
let vertexData = data.map(d => ([
// Textures go CCW
d[0] + 1.0, d[1] + 0.0,
d[0] + 0.0, d[1] + 0.0,
d[0] + 0.0, d[1] + 1.0,
d[0] + 1.0, d[1] + 1.0
]))
.flat()
.map(v => v / units);
return buf(gl, gl.ARRAY_BUFFER, new Float32Array(vertexData));
}
/**
* @param {WebGLRenderingContext} gl
* @param {number[]} data - Prebaked lighting, one value 0 - 255 for each rect
* @returns {WebGLBuffer}
*/
function lightingBuf(gl, data) {
let vertexData = data
.map(v => v / 255) // Multiplier, 0 - 1
.map(v => [v, v, v]) // For each color channel
.map(v => [v, v, v, v]) // For each vertex per rect
.flat(2);
return buf(gl, gl.ARRAY_BUFFER, new Float32Array(vertexData));
}