/****************************************************************************** * Spine Runtimes License Agreement * Last updated April 5, 2025. Replaces all prior versions. * * Copyright (c) 2013-2025, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and * conditions of Section 2 of the Spine Editor License Agreement: * http://esotericsoftware.com/spine-editor-license * * Otherwise, it is permitted to integrate the Spine Runtimes into software * or otherwise create derivative works of the Spine Runtimes (collectively, * "Products"), provided that each user of the Products must obtain their own * Spine Editor license and redistribution of the Products in any form must * include this license and copyright notice. * * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ import { BlendMode } from "@esotericsoftware/spine-core"; import * as THREE from "three" import { SkeletonMesh } from "./SkeletonMesh.js"; import { ThreeBlendOptions, ThreeJsTexture } from "./ThreeJsTexture.js"; export type MaterialWithMap = THREE.Material & { map: THREE.Texture | null }; export class MeshBatcher extends THREE.Mesh { public static MAX_VERTICES = 10920; private vertexSize = 9; private vertexBuffer: THREE.InterleavedBuffer; private vertices: Float32Array; private verticesLength = 0; private indices: Uint16Array; private indicesLength = 0; private materialGroups: [number, number, number][] = []; constructor ( maxVertices: number = MeshBatcher.MAX_VERTICES, private materialFactory: (parameters: THREE.MaterialParameters) => MaterialWithMap, private twoColorTint = true, ) { super(); if (maxVertices > MeshBatcher.MAX_VERTICES) throw new Error("Can't have more than 10920 triangles per batch: " + maxVertices); if (twoColorTint) { this.vertexSize += 3; } this.vertices = new Float32Array(maxVertices * this.vertexSize); this.indices = new Uint16Array(maxVertices * 3); const normals = new Float32Array(maxVertices * 3); for (let i = 0; i < maxVertices * 3; i += 3) { normals[i] = 0; normals[i + 1] = 0; normals[i + 2] = -1; } const vertexBuffer = new THREE.InterleavedBuffer(this.vertices, this.vertexSize) this.vertexBuffer = vertexBuffer; this.vertexBuffer.usage = WebGLRenderingContext.DYNAMIC_DRAW; const geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.InterleavedBufferAttribute(vertexBuffer, 3, 0, false)); geo.setAttribute("color", new THREE.InterleavedBufferAttribute(vertexBuffer, 4, 3, false)); geo.setAttribute("uv", new THREE.InterleavedBufferAttribute(vertexBuffer, 2, 7, false)); if (twoColorTint) geo.setAttribute("darkcolor", new THREE.InterleavedBufferAttribute(vertexBuffer, 3, 9, false)); const normalBuffer = new THREE.BufferAttribute(normals, 3); normalBuffer.usage = WebGLRenderingContext.STATIC_DRAW; geo.setAttribute("normal", normalBuffer); const indexBuffer = new THREE.BufferAttribute(this.indices, 1); indexBuffer.usage = WebGLRenderingContext.DYNAMIC_DRAW; geo.setIndex(indexBuffer); geo.drawRange.start = 0; geo.drawRange.count = 0; this.geometry = geo; this.material = []; } dispose () { this.geometry.dispose(); if (this.material instanceof THREE.Material) this.material.dispose(); else if (this.material) { for (let i = 0; i < this.material.length; i++) { let material = this.material[i]; if (material instanceof THREE.Material) material.dispose(); } } } clear () { let geo = (this.geometry); geo.drawRange.start = 0; geo.drawRange.count = 0; geo.clearGroups(); this.materialGroups = []; if (this.material instanceof THREE.Material) { const meshMaterial = this.material as MaterialWithMap; meshMaterial.map = null; meshMaterial.blending = THREE.NormalBlending; } else if (Array.isArray(this.material)) { for (let i = 0; i < this.material.length; i++) { const meshMaterial = this.material[i] as MaterialWithMap; meshMaterial.map = null; meshMaterial.blending = THREE.NormalBlending; } } return this; } begin () { this.verticesLength = 0; this.indicesLength = 0; } canBatch (numVertices: number, numIndices: number) { if (this.indicesLength + numIndices >= this.indices.byteLength / 2) return false; if (this.verticesLength / this.vertexSize + numVertices >= (this.vertices.byteLength / 4) / this.vertexSize) return false; return true; } batch (vertices: ArrayLike, verticesLength: number, indices: ArrayLike, indicesLength: number, z: number = 0) { let indexStart = this.verticesLength / this.vertexSize; let vertexBuffer = this.vertices; let i = this.verticesLength; let j = 0; if (this.twoColorTint) { for (; j < verticesLength;) { vertexBuffer[i++] = vertices[j++]; // x vertexBuffer[i++] = vertices[j++]; // y vertexBuffer[i++] = z; // z vertexBuffer[i++] = vertices[j++]; // r vertexBuffer[i++] = vertices[j++]; // g vertexBuffer[i++] = vertices[j++]; // b vertexBuffer[i++] = vertices[j++]; // a vertexBuffer[i++] = vertices[j++]; // u vertexBuffer[i++] = vertices[j++]; // v vertexBuffer[i++] = vertices[j++]; // dark r vertexBuffer[i++] = vertices[j++]; // dark g vertexBuffer[i++] = vertices[j++]; // dark b j++; } } else { for (; j < verticesLength;) { vertexBuffer[i++] = vertices[j++]; // x vertexBuffer[i++] = vertices[j++]; // y vertexBuffer[i++] = z; // z vertexBuffer[i++] = vertices[j++]; // r vertexBuffer[i++] = vertices[j++]; // g vertexBuffer[i++] = vertices[j++]; // b vertexBuffer[i++] = vertices[j++]; // a vertexBuffer[i++] = vertices[j++]; // u vertexBuffer[i++] = vertices[j++]; // v } } this.verticesLength = i; let indicesArray = this.indices; for (i = this.indicesLength, j = 0; j < indicesLength; i++, j++) indicesArray[i] = indices[j] + indexStart; this.indicesLength += indicesLength; } end () { this.vertexBuffer.needsUpdate = this.verticesLength > 0; this.vertexBuffer.addUpdateRange(0, this.verticesLength); const geo = (this.geometry); this.closeMaterialGroups(); const index = geo.getIndex(); if (!index) throw new Error("BufferAttribute must not be null."); index.needsUpdate = this.indicesLength > 0; index.addUpdateRange(0, this.indicesLength); geo.drawRange.start = 0; geo.drawRange.count = this.indicesLength; } addMaterialGroup (indicesLength: number, materialGroup: number) { const currentGroup = this.materialGroups[this.materialGroups.length - 1]; if (currentGroup === undefined || currentGroup[2] !== materialGroup) { this.materialGroups.push([this.indicesLength, indicesLength, materialGroup]); } else { currentGroup[1] += indicesLength; } } private closeMaterialGroups () { const geometry = this.geometry as THREE.BufferGeometry; for (let i = 0; i < this.materialGroups.length; i++) { const [startIndex, count, materialGroup] = this.materialGroups[i]; geometry.addGroup(startIndex, count, materialGroup); } } findMaterialGroup (slotTexture: THREE.Texture, slotBlendMode: BlendMode) { const blendingObject = ThreeJsTexture.toThreeJsBlending(slotBlendMode); let group = -1; if (Array.isArray(this.material)) { for (let i = 0; i < this.material.length; i++) { const meshMaterial = this.material[i] as MaterialWithMap; if (!meshMaterial.map) { updateMeshMaterial(meshMaterial, slotTexture, blendingObject); return i; } if (meshMaterial.map === slotTexture && blendingObject.blending === meshMaterial.blending && (blendingObject.blendSrc === undefined || blendingObject.blendSrc === meshMaterial.blendSrc) && (blendingObject.blendDst === undefined || blendingObject.blendDst === meshMaterial.blendDst) && (blendingObject.blendSrcAlpha === undefined || blendingObject.blendSrcAlpha === meshMaterial.blendSrcAlpha) && (blendingObject.blendDstAlpha === undefined || blendingObject.blendDstAlpha === meshMaterial.blendDstAlpha) ) { return i; } } const meshMaterial = this.newMaterial(); updateMeshMaterial(meshMaterial as MaterialWithMap, slotTexture, blendingObject); this.material.push(meshMaterial); group = this.material.length - 1; } else { throw new Error("MeshBatcher.material needs to be an array for geometry groups to work"); } return group; } private newMaterial (): MaterialWithMap { const meshMaterial = this.materialFactory(SkeletonMesh.DEFAULT_MATERIAL_PARAMETERS); if (!('map' in meshMaterial)) { throw new Error("The material factory must return a material having the map property for the texture."); } if (meshMaterial instanceof SkeletonMeshMaterial) { return meshMaterial; } if (this.twoColorTint) { meshMaterial.defines = { ...meshMaterial.defines, USE_SPINE_DARK_TINT: 1, } } meshMaterial.onBeforeCompile = spineOnBeforeCompile; return meshMaterial; } } const spineOnBeforeCompile = (shader: THREE.WebGLProgramParametersWithUniforms) => { let code; // VERTEX SHADER MODIFICATIONS // Add dark color attribute shader.vertexShader = ` #if defined( USE_SPINE_DARK_TINT ) attribute vec3 darkcolor; #endif ` + shader.vertexShader; // Add dark color attribute code = ` #if defined( USE_SPINE_DARK_TINT ) varying vec3 v_dark; #endif `; shader.vertexShader = insertAfterElementInShader(shader.vertexShader, '#include ', code); // Define v_dark varying code = ` #if defined( USE_SPINE_DARK_TINT ) v_dark = vec3( 1.0 ); v_dark *= darkcolor; #endif `; shader.vertexShader = insertAfterElementInShader(shader.vertexShader, '#include ', code); // FRAGMENT SHADER MODIFICATIONS // Define v_dark varying code = ` #ifdef USE_SPINE_DARK_TINT varying vec3 v_dark; #endif `; shader.fragmentShader = insertAfterElementInShader(shader.fragmentShader, '#include ', code); // Replacing color_fragment with the addition of dark tint formula if twoColorTint is true shader.fragmentShader = shader.fragmentShader.replace( '#include ', ` #ifdef USE_SPINE_DARK_TINT #ifdef USE_COLOR_ALPHA diffuseColor.a *= vColor.a; diffuseColor.rgb = (diffuseColor.a - diffuseColor.rgb) * v_dark.rgb + diffuseColor.rgb * vColor.rgb; #endif #else #ifdef USE_COLOR_ALPHA diffuseColor *= vColor; #endif #endif ` ); // We had to remove this because we need premultiplied blending modes, but our textures are already premultiplied // We could actually create a custom blending mode for Normal and Additive too shader.fragmentShader = shader.fragmentShader.replace('#include ', ''); // We had to remove this (and don't assign a color space to the texture) otherwise we would see artifacts on texture edges shader.fragmentShader = shader.fragmentShader.replace('#include ', ''); } function insertAfterElementInShader (shader: string, elementToFind: string, codeToInsert: string) { const index = shader.indexOf(elementToFind); const beforeToken = shader.slice(0, index + elementToFind.length); const afterToken = shader.slice(index + elementToFind.length); return beforeToken + codeToInsert + afterToken; } function updateMeshMaterial (meshMaterial: MaterialWithMap, slotTexture: THREE.Texture, blending: ThreeBlendOptions) { meshMaterial.map = slotTexture; Object.assign(meshMaterial, blending); meshMaterial.needsUpdate = true; } export class SkeletonMeshMaterial extends THREE.ShaderMaterial { public get map (): THREE.Texture | null { return this.uniforms.map.value; } public set map (value: THREE.Texture | null) { this.uniforms.map.value = value; } constructor (parameters: THREE.ShaderMaterialParameters) { let vertexShader = ` varying vec2 vUv; varying vec4 vColor; void main() { vUv = uv; vColor = color; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); } `; let fragmentShader = ` uniform sampler2D map; #ifdef USE_SPINE_ALPHATEST uniform float alphaTest; #endif varying vec2 vUv; varying vec4 vColor; void main(void) { gl_FragColor = texture2D(map, vUv)*vColor; #ifdef USE_SPINE_ALPHATEST if (gl_FragColor.a < alphaTest) discard; #endif } `; let uniforms = { map: { value: null } }; if (parameters.uniforms) { uniforms = { ...parameters.uniforms, ...uniforms }; } if (parameters.alphaTest && parameters.alphaTest > 0) { parameters.defines = { USE_SPINE_ALPHATEST: 1 }; } super({ vertexShader, fragmentShader, ...parameters, uniforms, }); } }