UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

475 lines (474 loc) 20.2 kB
import { VertexBuffer } from "../../../Buffers/buffer.js"; import { Texture } from "../texture.js"; import { DynamicTexture } from "../dynamicTexture.js"; import { Vector2 } from "../../../Maths/math.vector.js"; import { Color3, Color4 } from "../../../Maths/math.color.js"; import { TexturePackerFrame } from "./frame.js"; import { Logger } from "../../../Misc/logger.js"; import { Tools } from "../../../Misc/tools.js"; /** * This is a support class that generates a series of packed texture sets. * @see https://doc.babylonjs.com/features/featuresDeepDive/materials/using/materials_introduction */ export class TexturePacker { /** * Initializes a texture package series from an array of meshes or a single mesh. * @param name The name of the package * @param meshes The target meshes to compose the package from * @param options The arguments that texture packer should follow while building. * @param scene The scene which the textures are scoped to. * @returns TexturePacker */ constructor(name, meshes, options, scene) { this.name = name; this.meshes = meshes; this.scene = scene; /** * Run through the options and set what ever defaults are needed that where not declared. */ this.options = options; this.options.map = this.options.map ?? [ "ambientTexture", "bumpTexture", "diffuseTexture", "emissiveTexture", "lightmapTexture", "opacityTexture", "reflectionTexture", "refractionTexture", "specularTexture", ]; this.options.uvsIn = this.options.uvsIn ?? VertexBuffer.UVKind; this.options.uvsOut = this.options.uvsOut ?? VertexBuffer.UVKind; this.options.layout = this.options.layout ?? TexturePacker.LAYOUT_STRIP; if (this.options.layout === TexturePacker.LAYOUT_COLNUM) { this.options.colnum = this.options.colnum ?? 8; } this.options.updateInputMeshes = this.options.updateInputMeshes ?? true; this.options.disposeSources = this.options.disposeSources ?? true; this._expecting = 0; this.options.fillBlanks = this.options.fillBlanks ?? true; if (this.options.fillBlanks === true) { this.options.customFillColor = this.options.customFillColor ?? "black"; } this.options.frameSize = this.options.frameSize ?? 256; this.options.paddingRatio = this.options.paddingRatio ?? 0.0115; this._paddingValue = Math.ceil(this.options.frameSize * this.options.paddingRatio); //Make it an even padding Number. if (this._paddingValue % 2 !== 0) { this._paddingValue++; } this.options.paddingMode = this.options.paddingMode ?? TexturePacker.SUBUV_WRAP; if (this.options.paddingMode === TexturePacker.SUBUV_COLOR) { this.options.paddingColor = this.options.paddingColor ?? new Color4(0, 0, 0, 1.0); } this.sets = {}; this.frames = []; return this; } /** * Starts the package process * @param resolve The promises resolution function */ _createFrames(resolve) { const dtSize = this._calculateSize(); const dtUnits = new Vector2(1, 1).divide(dtSize); let doneCount = 0; const expecting = this._expecting; const meshLength = this.meshes.length; const sKeys = Object.keys(this.sets); for (let i = 0; i < sKeys.length; i++) { const setName = sKeys[i]; const dt = new DynamicTexture(this.name + ".TexturePack." + setName + "Set", { width: dtSize.x, height: dtSize.y }, this.scene, true, //Generate Mips Texture.TRILINEAR_SAMPLINGMODE, 5); const dtx = dt.getContext(); dtx.fillStyle = "rgba(0,0,0,0)"; dtx.fillRect(0, 0, dtSize.x, dtSize.y); dt.update(false); this.sets[setName] = dt; } const baseSize = this.options.frameSize || 256; const padding = this._paddingValue; const tcs = baseSize + 2 * padding; const done = () => { this._calculateMeshUVFrames(baseSize, padding, dtSize, dtUnits, this.options.updateInputMeshes || false); }; //Update the Textures for (let i = 0; i < meshLength; i++) { const m = this.meshes[i]; const mat = m.material; //Check if the material has the texture //Create a temporary canvas the same size as 1 frame //Then apply the texture to the center and the 8 offsets //Copy the Context and place in the correct frame on the DT for (let j = 0; j < sKeys.length; j++) { const tempTexture = new DynamicTexture("temp", tcs, this.scene, true); const tcx = tempTexture.getContext(); const offset = this._getFrameOffset(i); const updateDt = () => { doneCount++; tempTexture.update(false); const iDat = tcx.getImageData(0, 0, tcs, tcs); //Update Set const dt = this.sets[setName]; const dtx = dt.getContext(); dtx.putImageData(iDat, dtSize.x * offset.x, dtSize.y * offset.y); tempTexture.dispose(); dt.update(false); if (doneCount == expecting) { done(); resolve(); return; } }; const setName = sKeys[j] || "_blank"; if (!mat || mat[setName] === null) { tcx.fillStyle = "rgba(0,0,0,0)"; if (this.options.fillBlanks) { tcx.fillStyle = this.options.customFillColor; } tcx.fillRect(0, 0, tcs, tcs); updateDt(); } else { const setTexture = mat[setName]; const img = new Image(); if (setTexture instanceof DynamicTexture) { img.src = setTexture.getContext().canvas.toDataURL("image/png"); } else { img.src = setTexture.url; } Tools.SetCorsBehavior(img.src, img); img.onload = () => { tcx.fillStyle = "rgba(0,0,0,0)"; tcx.fillRect(0, 0, tcs, tcs); tempTexture.update(false); tcx.setTransform(1, 0, 0, -1, 0, 0); const cellOffsets = [0, 0, 1, 0, 1, 1, 0, 1, -1, 1, -1, 0, -1 - 1, 0, -1, 1, -1]; switch (this.options.paddingMode) { //Wrap Mode case 0: for (let i = 0; i < 9; i++) { tcx.drawImage(img, 0, 0, img.width, img.height, padding + baseSize * cellOffsets[i], padding + baseSize * cellOffsets[i + 1] - tcs, baseSize, baseSize); } break; //Extend Mode case 1: for (let i = 0; i < padding; i++) { tcx.drawImage(img, 0, 0, img.width, img.height, i + baseSize * cellOffsets[0], padding - tcs, baseSize, baseSize); tcx.drawImage(img, 0, 0, img.width, img.height, padding * 2 - i, padding - tcs, baseSize, baseSize); tcx.drawImage(img, 0, 0, img.width, img.height, padding, i - tcs, baseSize, baseSize); tcx.drawImage(img, 0, 0, img.width, img.height, padding, padding * 2 - i - tcs, baseSize, baseSize); } tcx.drawImage(img, 0, 0, img.width, img.height, padding + baseSize * cellOffsets[0], padding + baseSize * cellOffsets[1] - tcs, baseSize, baseSize); break; //Color Mode case 2: tcx.fillStyle = (this.options.paddingColor || Color3.Black()).toHexString(); tcx.fillRect(0, 0, tcs, -tcs); tcx.clearRect(padding, padding, baseSize, baseSize); tcx.drawImage(img, 0, 0, img.width, img.height, padding + baseSize * cellOffsets[0], padding + baseSize * cellOffsets[1] - tcs, baseSize, baseSize); break; } tcx.setTransform(1, 0, 0, 1, 0, 0); updateDt(); }; } } } } /** * Calculates the Size of the Channel Sets * @returns Vector2 */ _calculateSize() { const meshLength = this.meshes.length || 0; const baseSize = this.options.frameSize || 0; const padding = this._paddingValue || 0; switch (this.options.layout) { case 0: { //STRIP_LAYOUT return new Vector2(baseSize * meshLength + 2 * padding * meshLength, baseSize + 2 * padding); } case 1: { //POWER2 const sqrtCount = Math.max(2, Math.ceil(Math.sqrt(meshLength))); const size = baseSize * sqrtCount + 2 * padding * sqrtCount; return new Vector2(size, size); } case 2: { //COLNUM const cols = this.options.colnum || 1; const rowCnt = Math.max(1, Math.ceil(meshLength / cols)); return new Vector2(baseSize * cols + 2 * padding * cols, baseSize * rowCnt + 2 * padding * rowCnt); } } return Vector2.Zero(); } /** * Calculates the UV data for the frames. * @param baseSize the base frameSize * @param padding the base frame padding * @param dtSize size of the Dynamic Texture for that channel * @param dtUnits is 1/dtSize * @param update flag to update the input meshes */ _calculateMeshUVFrames(baseSize, padding, dtSize, dtUnits, update) { const meshLength = this.meshes.length; for (let i = 0; i < meshLength; i++) { const m = this.meshes[i]; const scale = new Vector2(baseSize / dtSize.x, baseSize / dtSize.y); const pOffset = dtUnits.clone().scale(padding); const frameOffset = this._getFrameOffset(i); const offset = frameOffset.add(pOffset); const frame = new TexturePackerFrame(i, scale, offset); this.frames.push(frame); //Update Output UVs if (update) { this._updateMeshUV(m, i); this._updateTextureReferences(m); } } } /** * Calculates the frames Offset. * @param index of the frame * @returns Vector2 */ _getFrameOffset(index) { const meshLength = this.meshes.length; let uvStep, yStep, xStep; switch (this.options.layout) { case 0: { //STRIP_LAYOUT uvStep = 1 / meshLength; return new Vector2(index * uvStep, 0); } case 1: { //POWER2 const sqrtCount = Math.max(2, Math.ceil(Math.sqrt(meshLength))); yStep = Math.floor(index / sqrtCount); xStep = index - yStep * sqrtCount; uvStep = 1 / sqrtCount; return new Vector2(xStep * uvStep, yStep * uvStep); } case 2: { //COLNUM const cols = this.options.colnum || 1; const rowCnt = Math.max(1, Math.ceil(meshLength / cols)); xStep = Math.floor(index / rowCnt); yStep = index - xStep * rowCnt; uvStep = new Vector2(1 / cols, 1 / rowCnt); return new Vector2(xStep * uvStep.x, yStep * uvStep.y); } } return Vector2.Zero(); } /** * Updates a Mesh to the frame data * @param mesh that is the target * @param frameID or the frame index */ _updateMeshUV(mesh, frameID) { const frame = this.frames[frameID]; const uvIn = mesh.getVerticesData(this.options.uvsIn || VertexBuffer.UVKind); const uvOut = []; let toCount = 0; if (uvIn.length) { toCount = uvIn.length || 0; } for (let i = 0; i < toCount; i += 2) { uvOut.push(uvIn[i] * frame.scale.x + frame.offset.x, uvIn[i + 1] * frame.scale.y + frame.offset.y); } mesh.setVerticesData(this.options.uvsOut || VertexBuffer.UVKind, uvOut); } /** * Updates a Meshes materials to use the texture packer channels * @param m is the mesh to target * @param force all channels on the packer to be set. */ _updateTextureReferences(m, force = false) { const mat = m.material; const sKeys = Object.keys(this.sets); const _dispose = (_t) => { if (_t.dispose) { _t.dispose(); } }; for (let i = 0; i < sKeys.length; i++) { const setName = sKeys[i]; if (!force) { if (!mat) { return; } if (mat[setName] !== null) { _dispose(mat[setName]); mat[setName] = this.sets[setName]; } } else { if (mat[setName] !== null) { _dispose(mat[setName]); } mat[setName] = this.sets[setName]; } } } /** * Public method to set a Mesh to a frame * @param m that is the target * @param frameID or the frame index * @param updateMaterial trigger for if the Meshes attached Material be updated? */ setMeshToFrame(m, frameID, updateMaterial = false) { this._updateMeshUV(m, frameID); if (updateMaterial) { this._updateTextureReferences(m, true); } } /** * Starts the async promise to compile the texture packer. * @returns Promise<void> */ processAsync() { return new Promise((resolve, reject) => { try { if (this.meshes.length === 0) { //Must be a JSON load! resolve(); return; } let done = 0; const doneCheck = (mat) => { done++; //Check Status of all Textures on all meshes, till they are ready. if (this.options.map) { for (let j = 0; j < this.options.map.length; j++) { const index = this.options.map[j]; const t = mat[index]; if (t !== null) { if (!this.sets[this.options.map[j]]) { this.sets[this.options.map[j]] = true; } this._expecting++; } } if (done === this.meshes.length) { this._createFrames(resolve); } } }; for (let i = 0; i < this.meshes.length; i++) { const mesh = this.meshes[i]; const material = mesh.material; if (!material) { done++; if (done === this.meshes.length) { return this._createFrames(resolve); } continue; } material.forceCompilationAsync(mesh).then(() => { doneCheck(material); }); } } catch (e) { return reject(e); } }); } /** * Disposes all textures associated with this packer */ dispose() { const sKeys = Object.keys(this.sets); for (let i = 0; i < sKeys.length; i++) { const channel = sKeys[i]; this.sets[channel].dispose(); } } /** * Starts the download process for all the channels converting them to base64 data and embedding it all in a JSON file. * @param imageType is the image type to use. * @param quality of the image if downloading as jpeg, Ranges from >0 to 1. */ download(imageType = "png", quality = 1) { setTimeout(() => { const pack = { name: this.name, sets: {}, options: {}, frames: [], }; const sKeys = Object.keys(this.sets); const oKeys = Object.keys(this.options); try { for (let i = 0; i < sKeys.length; i++) { const channel = sKeys[i]; const dt = this.sets[channel]; pack.sets[channel] = dt.getContext().canvas.toDataURL("image/" + imageType, quality); } for (let i = 0; i < oKeys.length; i++) { const opt = oKeys[i]; pack.options[opt] = this.options[opt]; } for (let i = 0; i < this.frames.length; i++) { const _f = this.frames[i]; pack.frames.push(_f.scale.x, _f.scale.y, _f.offset.x, _f.offset.y); } } catch (err) { Logger.Warn("Unable to download: " + err); return; } const data = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(pack, null, 4)); const _a = document.createElement("a"); _a.setAttribute("href", data); _a.setAttribute("download", this.name + "_texurePackage.json"); document.body.appendChild(_a); _a.click(); _a.remove(); }, 0); } /** * Public method to load a texturePacker JSON file. * @param data of the JSON file in string format. */ updateFromJSON(data) { try { const parsedData = JSON.parse(data); this.name = parsedData.name; const _options = Object.keys(parsedData.options); for (let i = 0; i < _options.length; i++) { this.options[_options[i]] = parsedData.options[_options[i]]; } for (let i = 0; i < parsedData.frames.length; i += 4) { const frame = new TexturePackerFrame(i / 4, new Vector2(parsedData.frames[i], parsedData.frames[i + 1]), new Vector2(parsedData.frames[i + 2], parsedData.frames[i + 3])); this.frames.push(frame); } const channels = Object.keys(parsedData.sets); for (let i = 0; i < channels.length; i++) { const _t = new Texture(parsedData.sets[channels[i]], this.scene, false, false); this.sets[channels[i]] = _t; } } catch (err) { Logger.Warn("Unable to update from JSON: " + err); } } } /** Packer Layout Constant 0 */ TexturePacker.LAYOUT_STRIP = 0; /** Packer Layout Constant 1 */ TexturePacker.LAYOUT_POWER2 = 1; /** Packer Layout Constant 2 */ TexturePacker.LAYOUT_COLNUM = 2; /** Packer Layout Constant 0 */ TexturePacker.SUBUV_WRAP = 0; /** Packer Layout Constant 1 */ TexturePacker.SUBUV_EXTEND = 1; /** Packer Layout Constant 2 */ TexturePacker.SUBUV_COLOR = 2; //# sourceMappingURL=packer.js.map