1+ /******************************************************************************
2+ * Spine Runtimes License Agreement
3+ * Last updated April 5, 2025. Replaces all prior versions.
4+ *
5+ * Copyright (c) 2013-2025, Esoteric Software LLC
6+ *
7+ * Integration of the Spine Runtimes into software or otherwise creating
8+ * derivative works of the Spine Runtimes is permitted under the terms and
9+ * conditions of Section 2 of the Spine Editor License Agreement:
10+ * http://esotericsoftware.com/spine-editor-license
11+ *
12+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
13+ * or otherwise create derivative works of the Spine Runtimes (collectively,
14+ * "Products"), provided that each user of the Products must obtain their own
15+ * Spine Editor license and redistribution of the Products in any form must
16+ * include this license and copyright notice.
17+ *
18+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
19+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
22+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
24+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
25+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+ *****************************************************************************/
29+
30+ import { ClippingAttachment , MeshAttachment , RegionAttachment } from "./attachments" ;
31+ import type { Skeleton } from "./Skeleton" ;
32+ import { SkeletonClipping } from "./SkeletonClipping" ;
33+ import { BlendMode } from "./SlotData" ;
34+ import type { Color , NumberArrayLike } from "./Utils" ;
35+
36+ export class SkeletonRendererCore {
37+ private commandPool = new CommandPool ( ) ;
38+ private worldVertices = new Float32Array ( 12 * 1024 ) ;
39+ private quadIndices = new Uint32Array ( [ 0 , 1 , 2 , 2 , 3 , 0 ] ) ;
40+ private clipping = new SkeletonClipping ( ) ;
41+ private renderCommands : RenderCommand [ ] = [ ] ;
42+
43+ render ( skeleton : Skeleton ) : RenderCommand | undefined {
44+ this . commandPool . reset ( ) ;
45+ this . renderCommands . length = 0 ;
46+
47+ const clipper = this . clipping ;
48+
49+ for ( let i = 0 ; i < skeleton . slots . length ; i ++ ) {
50+ const slot = skeleton . drawOrder [ i ] ;
51+ const attachment = slot . applied . attachment ;
52+
53+ if ( ! attachment ) {
54+ clipper . clipEnd ( slot ) ;
55+ continue ;
56+ }
57+
58+ const slotApplied = slot . applied ;
59+ const color = slotApplied . color ;
60+ const alpha = color . a ;
61+ if ( ( alpha === 0 || ! slot . bone . active ) && ! ( attachment instanceof ClippingAttachment ) ) {
62+ clipper . clipEnd ( slot ) ;
63+ continue ;
64+ }
65+
66+ let vertices : NumberArrayLike ;
67+ let verticesCount : number ;
68+ let uvs : NumberArrayLike ;
69+ let indices : number [ ] | Uint32Array ;
70+ let indicesCount : number ;
71+ let attachmentColor : Color ;
72+ let texture : any ;
73+
74+ if ( attachment instanceof RegionAttachment ) {
75+ attachmentColor = attachment . color ;
76+
77+ if ( attachmentColor . a === 0 ) {
78+ clipper . clipEnd ( slot ) ;
79+ continue ;
80+ }
81+
82+ attachment . computeWorldVertices ( slot , this . worldVertices , 0 , 2 ) ;
83+ vertices = this . worldVertices ;
84+ verticesCount = 4 ;
85+ uvs = attachment . uvs as Float32Array ;
86+ indices = this . quadIndices ;
87+ indicesCount = 6 ;
88+ texture = attachment . region ?. texture ;
89+
90+ } else if ( attachment instanceof MeshAttachment ) {
91+ attachmentColor = attachment . color ;
92+
93+ if ( attachmentColor . a === 0 ) {
94+ clipper . clipEnd ( slot ) ;
95+ continue ;
96+ }
97+
98+ if ( this . worldVertices . length < attachment . worldVerticesLength )
99+ this . worldVertices = new Float32Array ( attachment . worldVerticesLength ) ;
100+
101+ attachment . computeWorldVertices ( skeleton , slot , 0 , attachment . worldVerticesLength , this . worldVertices , 0 , 2 ) ;
102+ vertices = this . worldVertices ;
103+ verticesCount = attachment . worldVerticesLength >> 1 ;
104+ uvs = attachment . uvs as Float32Array ;
105+ indices = attachment . triangles ;
106+ indicesCount = indices . length ;
107+ texture = attachment . region ?. texture ;
108+
109+ } else if ( attachment instanceof ClippingAttachment ) {
110+ clipper . clipStart ( skeleton , slot , attachment ) ;
111+ continue ;
112+ } else {
113+ continue ;
114+ }
115+
116+ const skelColor = skeleton . color ;
117+ const r = Math . floor ( skelColor . r * slotApplied . color . r * attachmentColor . r * 255 ) ;
118+ const g = Math . floor ( skelColor . g * slotApplied . color . g * attachmentColor . g * 255 ) ;
119+ const b = Math . floor ( skelColor . b * slotApplied . color . b * attachmentColor . b * 255 ) ;
120+ const a = Math . floor ( skelColor . a * slotApplied . color . a * attachmentColor . a * 255 ) ;
121+
122+ let darkColor = 0xff000000 ;
123+ if ( slotApplied . darkColor ) {
124+ const { r, g, b } = slotApplied . darkColor ;
125+ darkColor = 0xff000000 |
126+ ( Math . floor ( r * 255 ) << 16 ) |
127+ ( Math . floor ( g * 255 ) << 8 ) |
128+ Math . floor ( b * 255 ) ;
129+ }
130+
131+ if ( clipper . isClipping ( ) ) {
132+ clipper . clipTrianglesUnpacked ( vertices , indices , indicesCount , uvs ) ;
133+ vertices = clipper . clippedVerticesTyped ;
134+ verticesCount = clipper . clippedVerticesLength >> 1 ;
135+ uvs = clipper . clippedUVsTyped ;
136+ indices = clipper . clippedTrianglesTyped ;
137+ indicesCount = clipper . clippedTrianglesLength ;
138+ }
139+
140+ const cmd = this . commandPool . getCommand ( verticesCount , indicesCount ) ;
141+ cmd . blendMode = slot . data . blendMode ;
142+ cmd . texture = texture ;
143+
144+ cmd . positions . set ( vertices . subarray ( 0 , verticesCount << 1 ) ) ;
145+ cmd . uvs . set ( uvs . subarray ( 0 , verticesCount << 1 ) ) ;
146+
147+ for ( let j = 0 ; j < verticesCount ; j ++ ) {
148+ cmd . colors [ j ] = ( a << 24 ) | ( r << 16 ) | ( g << 8 ) | b ;
149+ cmd . darkColors [ j ] = darkColor ;
150+ }
151+
152+ if ( indices instanceof Uint16Array ) {
153+ cmd . indices . set ( indices . subarray ( 0 , indicesCount ) ) ;
154+ } else {
155+ cmd . indices . set ( indices . slice ( 0 , indicesCount ) ) ;
156+ }
157+
158+ this . renderCommands . push ( cmd ) ;
159+ clipper . clipEnd ( slot ) ;
160+ }
161+
162+ clipper . clipEnd ( ) ;
163+ return this . batchCommands ( ) ;
164+ }
165+
166+ private batchSubCommands ( commands : RenderCommand [ ] , first : number , last : number ,
167+ numVertices : number , numIndices : number ) : RenderCommand {
168+
169+ const firstCmd = commands [ first ] ;
170+ const batched = this . commandPool . getCommand ( numVertices , numIndices ) ;
171+
172+ batched . blendMode = firstCmd . blendMode ;
173+ batched . texture = firstCmd . texture ;
174+
175+ let positionsOffset = 0 ;
176+ let uvsOffset = 0 ;
177+ let colorsOffset = 0 ;
178+ let indicesOffset = 0 ;
179+ let vertexOffset = 0 ;
180+
181+ for ( let i = first ; i <= last ; i ++ ) {
182+ const cmd = commands [ i ] ;
183+
184+ batched . positions . set ( cmd . positions , positionsOffset ) ;
185+ positionsOffset += cmd . numVertices << 1 ;
186+
187+ batched . uvs . set ( cmd . uvs , uvsOffset ) ;
188+ uvsOffset += cmd . numVertices << 1 ;
189+
190+ batched . colors . set ( cmd . colors , colorsOffset ) ;
191+ batched . darkColors . set ( cmd . darkColors , colorsOffset ) ;
192+ colorsOffset += cmd . numVertices ;
193+
194+ // cannot fast copy - indices need vertex offset adjustment
195+ for ( let j = 0 ; j < cmd . numIndices ; j ++ )
196+ batched . indices [ indicesOffset + j ] = cmd . indices [ j ] + vertexOffset ;
197+
198+ indicesOffset += cmd . numIndices ;
199+ vertexOffset += cmd . numVertices ;
200+ }
201+
202+ return batched ;
203+ }
204+
205+ private batchCommands ( ) : RenderCommand | undefined {
206+ if ( this . renderCommands . length === 0 ) return undefined ;
207+
208+ let root : RenderCommand | undefined ;
209+ let last : RenderCommand | undefined ;
210+
211+ let first = this . renderCommands [ 0 ] ;
212+ let startIndex = 0 ;
213+ let i = 1 ;
214+ let numVertices = first . numVertices ;
215+ let numIndices = first . numIndices ;
216+
217+ while ( i <= this . renderCommands . length ) {
218+ const cmd = i < this . renderCommands . length ? this . renderCommands [ i ] : null ;
219+
220+ if ( cmd && cmd . numVertices === 0 && cmd . numIndices === 0 ) {
221+ i ++ ;
222+ continue ;
223+ }
224+
225+ const canBatch = cmd !== null &&
226+ cmd . texture === first . texture &&
227+ cmd . blendMode === first . blendMode &&
228+ cmd . colors [ 0 ] === first . colors [ 0 ] &&
229+ cmd . darkColors [ 0 ] === first . darkColors [ 0 ] &&
230+ numIndices + cmd . numIndices < 0xffff ;
231+ if ( canBatch ) {
232+ numVertices += cmd . numVertices ;
233+ numIndices += cmd . numIndices ;
234+ } else {
235+ const batched = this . batchSubCommands ( this . renderCommands , startIndex , i - 1 ,
236+ numVertices , numIndices ) ;
237+
238+ if ( ! last ) {
239+ root = last = batched ;
240+ } else {
241+ last . next = batched ;
242+ last = batched ;
243+ }
244+
245+ if ( i === this . renderCommands . length ) break ;
246+
247+ first = this . renderCommands [ i ] ;
248+ startIndex = i ;
249+ numVertices = first . numVertices ;
250+ numIndices = first . numIndices ;
251+ }
252+ i ++ ;
253+ }
254+
255+ return root ;
256+ }
257+ }
258+
259+ interface RenderCommand {
260+ positions : Float32Array ;
261+ uvs : Float32Array ;
262+ colors : Uint32Array ;
263+ darkColors : Uint32Array ;
264+ indices : Uint16Array ;
265+ _positions : Float32Array ;
266+ _uvs : Float32Array ;
267+ _colors : Uint32Array ;
268+ _darkColors : Uint32Array ;
269+ _indices : Uint16Array ;
270+ numVertices : number ;
271+ numIndices : number ;
272+ blendMode : BlendMode ;
273+ texture : any ;
274+ next ?: RenderCommand ;
275+ }
276+
277+ class CommandPool {
278+ private pool : RenderCommand [ ] = [ ] ;
279+ private inUse : RenderCommand [ ] = [ ] ;
280+
281+ getCommand ( numVertices : number , numIndices : number ) : RenderCommand {
282+ let cmd : RenderCommand | undefined ;
283+ for ( const c of this . pool ) {
284+ if ( c . _positions . length >= numVertices << 1 && c . _indices . length >= numIndices ) {
285+ cmd = c ;
286+ break ;
287+ }
288+ }
289+
290+ if ( ! cmd ) {
291+ const _positions = new Float32Array ( numVertices << 1 ) ;
292+ const _uvs = new Float32Array ( numVertices << 1 ) ;
293+ const _colors = new Uint32Array ( numVertices ) ;
294+ const _darkColors = new Uint32Array ( numVertices ) ;
295+ const _indices = new Uint16Array ( numIndices ) ;
296+ cmd = {
297+ positions : _positions ,
298+ uvs : _uvs ,
299+ colors : _colors ,
300+ darkColors : _darkColors ,
301+ indices : _indices ,
302+ _positions,
303+ _uvs,
304+ _colors,
305+ _darkColors,
306+ _indices,
307+ numVertices,
308+ numIndices,
309+ blendMode : BlendMode . Normal ,
310+ texture : null
311+ } ;
312+ } else {
313+ this . pool . splice ( this . pool . indexOf ( cmd ) , 1 ) ;
314+ cmd . next = undefined ;
315+ cmd . numVertices = numVertices ;
316+ cmd . numIndices = numIndices ;
317+
318+ cmd . positions = cmd . _positions . subarray ( 0 , numVertices << 1 ) ;
319+ cmd . uvs = cmd . _uvs . subarray ( 0 , numVertices * 2 ) ;
320+ cmd . colors = cmd . _colors . subarray ( 0 , numVertices ) ;
321+ cmd . darkColors = cmd . _darkColors . subarray ( 0 , numVertices ) ;
322+ cmd . indices = cmd . _indices . subarray ( 0 , numIndices ) ;
323+ }
324+
325+ this . inUse . push ( cmd ) ;
326+ return cmd ;
327+ }
328+
329+ reset ( ) : void {
330+ this . pool . push ( ...this . inUse ) ;
331+ this . inUse . length = 0 ;
332+ }
333+ }
0 commit comments