"use strict";
export { MODEL_CUBE, MODEL_ITEM, MODEL_SKULL_COMPACT, MODEL_SKULL_SKIN, ModelType } from "./models.js";
export { TextureAtlas, createAtlas, createTexture } from "./texture.js";
import { GLINT_TEXTURE, ModelType } from "./models.js";
import { draw, resetDraw } from "./draw.js";
import { setupShader, loadTextureIntoWebGL } from "./setup.js";
import { createTexture } from "./texture.js";
/**
* @typedef {Object} ModelData
* @property {ModelType} modelType - One of {@link MODEL_CUBE}, {@link MODEL_ITEM}, {@link MODEL_SKULL_COMPACT}, {@link MODEL_SKULL_SKIN}.
* @property {string|TexImageSource} texture - A URL to the model texture, a {@link TextureAtlas} cutout or an arbitrary image.
* @property {boolean} [enchanted=false] - Applies Minecraft's enchantment glint.
* @property {boolean} [inInventory=false] - Applies padding to match rendering of Minecraft items in inventory.
* @property {[number, number, number]} [translation=[0, 0, 0]] - Applies a translation to the model.
* @property {number} [scale=1] - Scales the model.
* @public
*/
/**
* Creates a new renderer.
* @param {HTMLCanvasElement} canvas - The canvas to render on.
* @param {ModelData|ModelData[]} models - What to render.
* @param {WebGLContextAttributes} [contextAttributes={antialias: false}] - Arbitrary WebGL context attributes.
* @returns {Renderer}
* @public
*/
export function createRenderer(canvas, models, contextAttributes) {
if (!Array.isArray(models)) {
models = [models];
}
return new WebGLRenderer(canvas, contextAttributes, models);
}
/**
* A model renderer. See {@link createRenderer}.
* @public
* @hideconstructor
*/
export class Renderer {
/**
* The models to render. Can be updated dynamically.
* @type {ModelData[]}
* @public
*/
models;
/**
* Creates a new renderer.
* @param {ModelData|ModelData[]} - The models to render.
* @private
*/
constructor(models) {
this.models = Array.isArray(models) ? models : [models];
}
/**
* Destroys the renderer.
* @public
*/
destroy() { }
/**
* Renders a single frame using this renderer.
* @param {HTMLCanvasElement} canvas - The canvas to render on.
* @param {ModelData|ModelData[]} models - What to render.
* @param {CanvasRenderingContext2DSettings} [contextAttributes={}] - Arbitrary 2D context attributes.
* @public
*/
renderShared(canvas, models, contextAttributes) { }
/**
* Creates a new renderer that uses this renderer to draw.
* This can help with WebGL context limits but may affect performance.
* @param {HTMLCanvasElement} canvas - The canvas to render on.
* @param {ModelData|ModelData[]} models - What to render.
* @param {CanvasRenderingContext2DSettings} [contextAttributes={}] - Arbitrary 2D context attributes.
* @returns {Renderer}
* @public
*/
createSharedRenderer(canvas, models, contextAttributes) { }
}
/**
* A renderer targeting WebGL canvases.
* @private
*/
class WebGLRenderer extends Renderer {
/**
* All {@link BlitRenderer}s using this renderer as a backend.
* @type {BlitRenderer[]}
* @private
*/
sharingRenderers;
/**
* The pixel buffer used when transfering the image to a 2D canvas.
* @type {Uint8ClampedArray | null}
* @private
*/
pixelBuffer;
/**
* Creates a new renderer.
* @param {HTMLCanvasElement} canvas - The canvas to render on.
* @param {WebGLContextAttributes} [contextAttributes={antialias: false}] - Arbitrary WebGL context attributes.
* @param {ModelData|ModelData[]} models - The models to render.
* @private
*/
constructor(canvas, contextAttributes, models) {
super(models);
this.sharingRenderers = [];
this.pixelBuffer = null;
if (!self?.glMatrix?.mat4) {
throw new Error("Could not find matrix library. Please ensure you include glMatrix v3.");
}
// Disable antialias by default
contextAttributes ||= {};
if (contextAttributes.antialias === undefined) {
contextAttributes.antialias = false;
}
const gl = canvas.getContext("webgl", contextAttributes);
if (gl === null) {
throw new Error("Could not initialize WebGL. Your browser may not support it.");
}
this.gl = gl;
this.programInfo = setupShader(gl);
// Start render loop
const render = async () => {
await this.renderFrame();
if (this.activeHandle)
this.activeHandle = requestAnimationFrame(render);
}
this.activeHandle = requestAnimationFrame(render);
}
/**
* @private
*/
async renderFrame() {
let w = this.gl.canvas.width;
let h = this.gl.canvas.height;
for (let blitRenderer of this.sharingRenderers) {
await this.renderModels(blitRenderer.models, true);
if (this.pixelBuffer == null) {
this.pixelBuffer = new Uint8ClampedArray(w * h * 4,);
}
this.gl.readPixels(0, 0, w, h, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.pixelBuffer);
blitRenderer.copy(this.pixelBuffer);
}
await this.renderModels(this.models, false);
}
/**
* @param {ModelData[]} - The models to render.
* @param {boolean} flip
* @private
*/
async renderModels(models, flip) {
resetDraw(this.gl, true);
for (let model of models) {
await this.prepareDraw(model);
model.draw(flip);
}
}
/**
* Initializes the ModelData#draw function.
* @param {ModelData} modelData
* @private
*/
async prepareDraw(modelData) {
if (modelData.draw) {
return;
}
// Load texture
const texSource = await createTexture(modelData.texture);
const texture = loadTextureIntoWebGL(this.gl, texSource, texSource.width, texSource.height);
const glintTexture = modelData.enchanted ? loadTextureIntoWebGL(this.gl, GLINT_TEXTURE, 64, 64) : null;
// Create draw call
const model = modelData.modelType.create(this.gl);
const translation = modelData.translation || [0, 0, 0];
const scaleMul = 1 / (modelData.scale || 1); // Take inverse of scale so that bigger scale = bigger model
modelData.draw = flip => draw(this.gl, this.programInfo, modelData.inInventory, model, texture, glintTexture, translation, scaleMul, flip);
}
destroy() {
if (this.activeHandle != null) {
cancelAnimationFrame(this.activeHandle);
this.activeHandle = null;
this.gl.getExtension("WEBGL_lose_context")?.loseContext();
}
}
renderShared(canvas, models, contextAttributes) {
new BlitRenderer(canvas, contextAttributes, this, models, true);
}
createSharedRenderer(canvas, models, contextAttributes) {
return new BlitRenderer(canvas, contextAttributes, this, models, false);
}
}
/**
* A renderer targeting 2D canvases by copying the image from a {@link WebGLRenderer}.
* @private
*/
class BlitRenderer extends Renderer {
/**
* Creates a new renderer.
* @param {HTMLCanvasElement} canvas - The canvas to render on.
* @param {CanvasRenderingContext2DSettings} [contextAttributes={}] - Arbitrary 2D context attributes.
* @param {WebGLRenderer} renderer - The underlying renderer.
* @param {ModelData|ModelData[]} models - The models to render.
* @param {boolean} oneshot - Destroys the renderer after the first frame.
* @private
*/
constructor(canvas, contextAttributes, renderer, models, oneshot) {
super(models);
this.oneshot = oneshot;
this.ctx = canvas.getContext("2d", contextAttributes || {});
this.backend = renderer;
this.backend.sharingRenderers.push(this);
}
/**
* Copies the image to the target canvas.
* @param {Uint8ClampedArray} pixels
*/
copy(pixels) {
this.ctx.putImageData(new ImageData(pixels, this.ctx.canvas.width, this.ctx.canvas.height), 0, 0);
if (this.oneshot) {
this.destroy();
}
}
destroy() {
let idx = this.backend.sharingRenderers.indexOf(this);
if (idx !== -1) {
this.backend.sharingRenderers.splice(idx, 1);
}
}
}