@lightningjs/renderer 2.19.1 → 2.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/src/common/CommonTypes.d.ts +11 -0
  2. package/dist/src/core/AutosizeManager.d.ts +29 -0
  3. package/dist/src/core/AutosizeManager.js +171 -0
  4. package/dist/src/core/AutosizeManager.js.map +1 -0
  5. package/dist/src/core/CoreNode.d.ts +1 -1
  6. package/dist/src/core/CoreNode.js +8 -0
  7. package/dist/src/core/CoreNode.js.map +1 -1
  8. package/dist/src/core/Stage.js +2 -1
  9. package/dist/src/core/Stage.js.map +1 -1
  10. package/dist/src/core/animations/Animation.d.ts +16 -0
  11. package/dist/src/core/animations/Animation.js +111 -0
  12. package/dist/src/core/animations/Animation.js.map +1 -0
  13. package/dist/src/core/animations/CoreTransition.d.ts +24 -0
  14. package/dist/src/core/animations/CoreTransition.js +63 -0
  15. package/dist/src/core/animations/CoreTransition.js.map +1 -0
  16. package/dist/src/core/animations/Playback.d.ts +62 -0
  17. package/dist/src/core/animations/Playback.js +155 -0
  18. package/dist/src/core/animations/Playback.js.map +1 -0
  19. package/dist/src/core/animations/Transition.d.ts +25 -0
  20. package/dist/src/core/animations/Transition.js +63 -0
  21. package/dist/src/core/animations/Transition.js.map +1 -0
  22. package/dist/src/core/animations/utils.d.ts +2 -0
  23. package/dist/src/core/animations/utils.js +137 -0
  24. package/dist/src/core/animations/utils.js.map +1 -0
  25. package/dist/src/core/lib/WebGlContextWrapper.d.ts +19 -5
  26. package/dist/src/core/lib/WebGlContextWrapper.js +35 -0
  27. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  28. package/dist/src/core/lib/collectionUtils.d.ts +5 -0
  29. package/dist/src/core/lib/collectionUtils.js +100 -0
  30. package/dist/src/core/lib/collectionUtils.js.map +1 -0
  31. package/dist/src/core/lib/textureCompression.d.ts +14 -2
  32. package/dist/src/core/lib/textureCompression.js +301 -65
  33. package/dist/src/core/lib/textureCompression.js.map +1 -1
  34. package/dist/src/core/platforms/Platform.d.ts +5 -0
  35. package/dist/src/core/platforms/Platform.js.map +1 -1
  36. package/dist/src/core/platforms/web/WebPlatform.d.ts +1 -0
  37. package/dist/src/core/platforms/web/WebPlatform.js +3 -0
  38. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  39. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.d.ts +4 -0
  40. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js +14 -11
  41. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js.map +1 -1
  42. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.js +1 -1
  43. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js +43 -8
  44. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js.map +1 -1
  45. package/dist/src/core/renderers/webgl/WebGlCoreShader.d.ts +2 -7
  46. package/dist/src/core/renderers/webgl/WebGlCoreShader.js +18 -49
  47. package/dist/src/core/renderers/webgl/WebGlCoreShader.js.map +1 -1
  48. package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +0 -2
  49. package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
  50. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js +3 -6
  51. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js.map +1 -1
  52. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js +0 -11
  53. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js.map +1 -1
  54. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js +5 -10
  55. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js.map +1 -1
  56. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js +5 -10
  57. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js.map +1 -1
  58. package/dist/src/core/renderers/webgl/shaders/SdfShader.js +0 -12
  59. package/dist/src/core/renderers/webgl/shaders/SdfShader.js.map +1 -1
  60. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.js +1 -1
  61. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.js +1 -1
  62. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.js +1 -1
  63. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.js +1 -1
  64. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.js +5 -5
  65. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.js +1 -1
  66. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.js +1 -1
  67. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.js +1 -1
  68. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js +1 -1
  69. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.js +5 -5
  70. package/dist/src/core/shaders/webgl/LinearGradient.js +2 -1
  71. package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
  72. package/dist/src/core/shaders/webgl/RadialGradient.js +7 -6
  73. package/dist/src/core/shaders/webgl/RadialGradient.js.map +1 -1
  74. package/dist/src/core/text-rendering/CanvasFontHandler.js +4 -1
  75. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  76. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +4 -6
  77. package/dist/src/core/text-rendering/CanvasTextRenderer.js +15 -23
  78. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  79. package/dist/src/core/text-rendering/SdfFontHandler.d.ts +125 -28
  80. package/dist/src/core/text-rendering/SdfFontHandler.js +337 -4
  81. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  82. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +5 -6
  83. package/dist/src/core/text-rendering/SdfTextRenderer.js +19 -23
  84. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  85. package/dist/src/core/text-rendering/TextLayoutEngine.d.ts +14 -16
  86. package/dist/src/core/text-rendering/TextLayoutEngine.js +248 -192
  87. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  88. package/dist/src/core/text-rendering/TextRenderer.d.ts +25 -11
  89. package/dist/src/core/textures/Texture.d.ts +12 -5
  90. package/dist/src/core/textures/Texture.js.map +1 -1
  91. package/dist/src/main-api/Inspector.js +9 -5
  92. package/dist/src/main-api/Inspector.js.map +1 -1
  93. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  94. package/package.json +1 -1
  95. package/src/common/CommonTypes.ts +16 -0
  96. package/src/core/CoreNode.test.ts +49 -0
  97. package/src/core/CoreNode.ts +10 -0
  98. package/src/core/Stage.ts +3 -1
  99. package/src/core/lib/WebGlContextWrapper.ts +49 -0
  100. package/src/core/lib/textureCompression.ts +416 -75
  101. package/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +16 -14
  102. package/src/core/renderers/webgl/WebGlCoreRenderOp.ts +1 -1
  103. package/src/core/renderers/webgl/WebGlCoreRenderer.ts +44 -11
  104. package/src/core/renderers/webgl/WebGlCoreShader.ts +27 -75
  105. package/src/core/renderers/webgl/internal/ShaderUtils.ts +0 -2
  106. package/src/core/renderers/webgl/shaders/DefaultShader.ts +3 -6
  107. package/src/core/renderers/webgl/shaders/DefaultShaderBatched.ts +0 -11
  108. package/src/core/renderers/webgl/shaders/DynamicShader.ts +5 -10
  109. package/src/core/renderers/webgl/shaders/RoundedRectangle.ts +5 -10
  110. package/src/core/renderers/webgl/shaders/SdfShader.ts +0 -12
  111. package/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.ts +1 -1
  112. package/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.ts +1 -1
  113. package/src/core/renderers/webgl/shaders/effects/BorderRightEffect.ts +1 -1
  114. package/src/core/renderers/webgl/shaders/effects/BorderTopEffect.ts +1 -1
  115. package/src/core/renderers/webgl/shaders/effects/FadeOutEffect.ts +5 -5
  116. package/src/core/renderers/webgl/shaders/effects/HolePunchEffect.ts +1 -1
  117. package/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.ts +1 -1
  118. package/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.ts +1 -1
  119. package/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.ts +1 -1
  120. package/src/core/renderers/webgl/shaders/effects/RadiusEffect.ts +5 -5
  121. package/src/core/textures/Texture.ts +13 -6
  122. package/src/main-api/Inspector.ts +9 -5
  123. package/dist/src/core/text-rendering/CanvasFont.d.ts +0 -14
  124. package/dist/src/core/text-rendering/CanvasFont.js +0 -111
  125. package/dist/src/core/text-rendering/CanvasFont.js.map +0 -1
  126. package/dist/src/core/text-rendering/CoreFont.d.ts +0 -33
  127. package/dist/src/core/text-rendering/CoreFont.js +0 -48
  128. package/dist/src/core/text-rendering/CoreFont.js.map +0 -1
  129. package/dist/src/core/text-rendering/FontManager.d.ts +0 -11
  130. package/dist/src/core/text-rendering/FontManager.js +0 -42
  131. package/dist/src/core/text-rendering/FontManager.js.map +0 -1
  132. package/dist/src/core/text-rendering/SdfFont.d.ts +0 -29
  133. package/dist/src/core/text-rendering/SdfFont.js +0 -142
  134. package/dist/src/core/text-rendering/SdfFont.js.map +0 -1
@@ -38,6 +38,7 @@ import type {
38
38
  NodeTextureFailedPayload,
39
39
  NodeTextureFreedPayload,
40
40
  NodeTextureLoadedPayload,
41
+ NodeRenderablePayload,
41
42
  } from '../common/CommonTypes.js';
42
43
  import { EventEmitter } from '../common/EventEmitter.js';
43
44
  import {
@@ -1515,7 +1516,16 @@ export class CoreNode extends EventEmitter {
1515
1516
  * @param isRenderable - The new renderable state
1516
1517
  */
1517
1518
  setRenderable(isRenderable: boolean) {
1519
+ const previousIsRenderable = this.isRenderable;
1518
1520
  this.isRenderable = isRenderable;
1521
+
1522
+ // Emit event if renderable status has changed
1523
+ if (previousIsRenderable !== isRenderable) {
1524
+ this.emit('renderable', {
1525
+ type: 'renderable',
1526
+ isRenderable,
1527
+ } satisfies NodeRenderablePayload);
1528
+ }
1519
1529
  }
1520
1530
 
1521
1531
  /**
package/src/core/Stage.ts CHANGED
@@ -579,7 +579,9 @@ export class Stage {
579
579
  if (nodes.length === 0) {
580
580
  return null;
581
581
  }
582
- let topNode = nodes[0] as CoreNode;
582
+
583
+ //get last node in array (as top node)
584
+ let topNode = nodes[nodes.length - 1] as CoreNode;
583
585
  for (let i = 0; i < nodes.length; i++) {
584
586
  if (nodes[i]!.zIndex > topNode.zIndex) {
585
587
  topNode = nodes[i]!;
@@ -67,6 +67,7 @@ export class WebGlContextWrapper {
67
67
  public readonly TEXTURE_WRAP_S;
68
68
  public readonly TEXTURE_WRAP_T;
69
69
  public readonly LINEAR;
70
+ public readonly LINEAR_MIPMAP_LINEAR;
70
71
  public readonly CLAMP_TO_EDGE;
71
72
  public readonly RGB;
72
73
  public readonly RGBA;
@@ -158,6 +159,7 @@ export class WebGlContextWrapper {
158
159
  this.TEXTURE_WRAP_S = gl.TEXTURE_WRAP_S;
159
160
  this.TEXTURE_WRAP_T = gl.TEXTURE_WRAP_T;
160
161
  this.LINEAR = gl.LINEAR;
162
+ this.LINEAR_MIPMAP_LINEAR = gl.LINEAR_MIPMAP_LINEAR;
161
163
  this.CLAMP_TO_EDGE = gl.CLAMP_TO_EDGE;
162
164
  this.RGB = gl.RGB;
163
165
  this.RGBA = gl.RGBA;
@@ -703,6 +705,53 @@ export class WebGlContextWrapper {
703
705
  gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
704
706
  }
705
707
 
708
+ /**
709
+ * Returns object with Attribute names as key and numbers as location values
710
+ *
711
+ * @param program
712
+ * @returns object with numbers
713
+ */
714
+ getUniformLocations(
715
+ program: WebGLProgram,
716
+ ): Record<string, WebGLUniformLocation> {
717
+ const gl = this.gl;
718
+ const length = gl.getProgramParameter(
719
+ program,
720
+ gl.ACTIVE_UNIFORMS,
721
+ ) as number;
722
+ const result = {} as Record<string, WebGLUniformLocation>;
723
+ for (let i = 0; i < length; i++) {
724
+ const info = gl.getActiveUniform(program, i) as WebGLActiveInfo;
725
+ //remove bracket + value from uniform name;
726
+ let name = info.name.replace(/\[.*?\]/g, '');
727
+ result[name] = gl.getUniformLocation(
728
+ program,
729
+ name,
730
+ ) as WebGLUniformLocation;
731
+ }
732
+ return result;
733
+ }
734
+
735
+ /**
736
+ * Returns object with Attribute names as key and numbers as location values
737
+ * @param program
738
+ * @returns object with numbers
739
+ */
740
+ getAttributeLocations(program: WebGLProgram): string[] {
741
+ const gl = this.gl;
742
+ const length = gl.getProgramParameter(
743
+ program,
744
+ gl.ACTIVE_ATTRIBUTES,
745
+ ) as number;
746
+
747
+ const result: string[] = [];
748
+ for (let i = 0; i < length; i++) {
749
+ const { name } = gl.getActiveAttrib(program, i) as WebGLActiveInfo;
750
+ result[gl.getAttribLocation(program, name)] = name;
751
+ }
752
+ return result;
753
+ }
754
+
706
755
  /**
707
756
  * ```
708
757
  * gl.useProgram(program);
@@ -16,8 +16,14 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  */
19
+ import { type CompressedData, type TextureData } from '../textures/Texture.js';
20
+ import type { WebGlContextWrapper } from './WebGlContextWrapper.js';
19
21
 
20
- import { type TextureData } from '../textures/Texture.js';
22
+ export type UploadCompressedTextureFunction = (
23
+ glw: WebGlContextWrapper,
24
+ texture: WebGLTexture,
25
+ data: CompressedData,
26
+ ) => void;
21
27
 
22
28
  /**
23
29
  * Tests if the given location is a compressed texture container
@@ -27,10 +33,38 @@ import { type TextureData } from '../textures/Texture.js';
27
33
  * and only supports the following extensions: .ktx and .pvr
28
34
  * @returns
29
35
  */
30
- export function isCompressedTextureContainer(url: string): boolean {
31
- return /\.(ktx|pvr)$/.test(url);
36
+ export function isCompressedTextureContainer(src: string): boolean {
37
+ return /\.(ktx|pvr)$/.test(src);
32
38
  }
33
39
 
40
+ const PVR_MAGIC = 0x03525650; // 'PVR3' in little-endian
41
+ const PVR_TO_GL_INTERNAL_FORMAT: Record<string, number> = {
42
+ 0: 0x8c01,
43
+ 1: 0x8c03,
44
+ 2: 0x8c00,
45
+ 3: 0x8c02, // PVRTC1
46
+ 6: 0x8d64, // ETC1
47
+ 7: 0x83f0,
48
+ 8: 0x83f2,
49
+ 9: 0x83f2,
50
+ 10: 0x83f3,
51
+ 11: 0x83f3, // DXT variants
52
+ };
53
+ const ASTC_MAGIC = 0x5ca1ab13;
54
+
55
+ const ASTC_TO_GL_INTERNAL_FORMAT: Record<string, number> = {
56
+ '4x4': 0x93b0, // COMPRESSED_RGBA_ASTC_4x4_KHR
57
+ '5x5': 0x93b1, // COMPRESSED_RGBA_ASTC_5x5_KHR
58
+ '6x6': 0x93b2, // COMPRESSED_RGBA_ASTC_6x6_KHR
59
+ '8x8': 0x93b3, // COMPRESSED_RGBA_ASTC_8x8_KHR
60
+ '10x10': 0x93b4, // COMPRESSED_RGBA_ASTC_10x10_KHR
61
+ '12x12': 0x93b5, // COMPRESSED_RGBA_ASTC_12x12_KHR
62
+ };
63
+
64
+ // KTX file identifier
65
+ const KTX_IDENTIFIER = [
66
+ 0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a,
67
+ ];
34
68
  /**
35
69
  * Loads a compressed texture container
36
70
  * @param url
@@ -41,7 +75,6 @@ export const loadCompressedTexture = async (
41
75
  ): Promise<TextureData> => {
42
76
  try {
43
77
  const response = await fetch(url);
44
-
45
78
  if (!response.ok) {
46
79
  throw new Error(
47
80
  `Failed to fetch compressed texture: ${response.status} ${response.statusText}`,
@@ -50,114 +83,422 @@ export const loadCompressedTexture = async (
50
83
 
51
84
  const arrayBuffer = await response.arrayBuffer();
52
85
 
53
- if (url.indexOf('.ktx') !== -1) {
54
- return loadKTXData(arrayBuffer);
86
+ // Ensure we have enough data to check magic numbers
87
+ if (arrayBuffer.byteLength < 16) {
88
+ throw new Error(
89
+ `File too small to be a valid compressed texture (${arrayBuffer.byteLength} bytes). Expected at least 16 bytes for header inspection.`,
90
+ );
91
+ }
92
+
93
+ const view = new DataView(arrayBuffer);
94
+ const magic = view.getUint32(0, true);
95
+
96
+ if (magic === PVR_MAGIC) {
97
+ return loadPVR(view);
98
+ }
99
+
100
+ if (magic === ASTC_MAGIC) {
101
+ return loadASTC(view);
102
+ }
103
+
104
+ let isKTX = true;
105
+
106
+ for (let i = 0; i < KTX_IDENTIFIER.length; i++) {
107
+ if (view.getUint8(i) !== KTX_IDENTIFIER[i]) {
108
+ isKTX = false;
109
+ break;
110
+ }
55
111
  }
56
112
 
57
- return loadPVRData(arrayBuffer);
113
+ if (isKTX === true) {
114
+ return loadKTX(view);
115
+ } else {
116
+ throw new Error('Unrecognized compressed texture format');
117
+ }
58
118
  } catch (error) {
59
119
  throw new Error(`Failed to load compressed texture from ${url}: ${error}`);
60
120
  }
61
121
  };
62
122
 
123
+ function readUint24(view: DataView, offset: number) {
124
+ return (
125
+ view.getUint8(offset) +
126
+ (view.getUint8(offset + 1) << 8) +
127
+ (view.getUint8(offset + 2) << 16)
128
+ );
129
+ }
130
+
63
131
  /**
64
- * Loads a KTX texture container and returns the texture data
65
- * @param buffer
132
+ * Loads an ASTC texture container and returns the texture data
133
+ * @param view
66
134
  * @returns
67
135
  */
68
- const loadKTXData = async (buffer: ArrayBuffer): Promise<TextureData> => {
69
- const view = new DataView(buffer);
70
- const littleEndian = view.getUint32(12) === 16909060 ? true : false;
71
- const mipmaps = [];
72
-
73
- const data = {
74
- glInternalFormat: view.getUint32(28, littleEndian),
75
- pixelWidth: view.getUint32(36, littleEndian),
76
- pixelHeight: view.getUint32(40, littleEndian),
77
- numberOfMipmapLevels: view.getUint32(56, littleEndian),
78
- bytesOfKeyValueData: view.getUint32(60, littleEndian),
79
- };
136
+ const loadASTC = async function (view: DataView): Promise<TextureData> {
137
+ const blockX = view.getUint8(4);
138
+ const blockY = view.getUint8(5);
139
+ const sizeX = readUint24(view, 7);
140
+ const sizeY = readUint24(view, 10);
80
141
 
81
- let offset = 64;
142
+ if (sizeX === 0 || sizeY === 0) {
143
+ throw new Error(`Invalid ASTC texture dimensions: ${sizeX}x${sizeY}`);
144
+ }
145
+ const expected = Math.ceil(sizeX / blockX) * Math.ceil(sizeY / blockY) * 16;
146
+ const dataSize = view.byteLength - 16;
147
+ if (expected !== dataSize) {
148
+ throw new Error(
149
+ `Invalid ASTC texture data size: expected ${expected}, got ${dataSize}`,
150
+ );
151
+ }
82
152
 
83
- // Key Value Pairs of data start at byte offset 64
84
- // But the only known kvp is the API version, so skipping parsing.
85
- offset += data.bytesOfKeyValueData;
153
+ const internalFormat = ASTC_TO_GL_INTERNAL_FORMAT[`${blockX}x${blockY}`];
154
+ if (internalFormat === undefined) {
155
+ throw new Error(`Unsupported ASTC block size: ${blockX}x${blockY}`);
156
+ }
86
157
 
87
- for (let i = 0; i < data.numberOfMipmapLevels; i++) {
88
- const imageSize = view.getUint32(offset);
89
- offset += 4;
158
+ const buffer = view.buffer as ArrayBuffer;
90
159
 
91
- mipmaps.push(view.buffer.slice(offset, imageSize));
92
- offset += imageSize;
93
- }
160
+ const mipmaps: ArrayBuffer[] = [];
161
+ mipmaps.push(buffer.slice(16));
94
162
 
95
163
  return {
96
164
  data: {
97
- glInternalFormat: data.glInternalFormat,
165
+ blockInfo: blockInfoMap[internalFormat]!,
166
+ glInternalFormat: internalFormat,
98
167
  mipmaps,
99
- width: data.pixelWidth || 0,
100
- height: data.pixelHeight || 0,
101
- type: 'ktx',
168
+ width: sizeX,
169
+ height: sizeY,
170
+ type: 'astc',
102
171
  },
103
172
  premultiplyAlpha: false,
104
173
  };
105
174
  };
106
175
 
176
+ const uploadASTC = function (
177
+ glw: WebGlContextWrapper,
178
+ texture: WebGLTexture,
179
+ data: CompressedData,
180
+ ) {
181
+ if (glw.getExtension('WEBGL_compressed_texture_astc') === null) {
182
+ throw new Error('ASTC compressed textures not supported by this device');
183
+ }
184
+
185
+ glw.bindTexture(texture);
186
+
187
+ const { glInternalFormat, mipmaps, width, height } = data;
188
+
189
+ const view = new Uint8Array(mipmaps[0]!);
190
+
191
+ glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view);
192
+
193
+ // ASTC textures MUST use no mipmaps unless stored
194
+ glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
195
+ glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
196
+ glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
197
+ glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR);
198
+ };
107
199
  /**
108
- * Loads a PVR texture container and returns the texture data
109
- * @param buffer
200
+ * Loads a KTX texture container and returns the texture data
201
+ * @param view
110
202
  * @returns
111
203
  */
112
- const loadPVRData = async (buffer: ArrayBuffer): Promise<TextureData> => {
113
- // pvr header length in 32 bits
114
- const pvrHeaderLength = 13;
115
- // for now only we only support: COMPRESSED_RGB_ETC1_WEBGL
116
- const pvrFormatEtc1 = 0x8d64;
117
- const pvrWidth = 7;
118
- const pvrHeight = 6;
119
- const pvrMipmapCount = 11;
120
- const pvrMetadata = 12;
121
- const arrayBuffer = buffer;
122
- const header = new Int32Array(arrayBuffer, 0, pvrHeaderLength);
123
-
124
- // @ts-expect-error Object possibly undefined
125
-
126
- const dataOffset = header[pvrMetadata] + 52;
127
- const pvrtcData = new Uint8Array(arrayBuffer, dataOffset);
128
- const mipmaps = [];
129
- const data = {
130
- pixelWidth: header[pvrWidth],
131
- pixelHeight: header[pvrHeight],
132
- numberOfMipmapLevels: header[pvrMipmapCount] || 0,
204
+ const loadKTX = async function (view: DataView): Promise<TextureData> {
205
+ const endianness = view.getUint32(12, true);
206
+ const littleEndian = endianness === 0x04030201;
207
+ if (littleEndian === false && endianness !== 0x01020304) {
208
+ throw new Error('Invalid KTX endianness value');
209
+ }
210
+
211
+ const glType = view.getUint32(16, littleEndian);
212
+ const glFormat = view.getUint32(24, littleEndian);
213
+ if (glType !== 0 || glFormat !== 0) {
214
+ throw new Error(
215
+ `KTX texture is not compressed (glType: ${glType}, glFormat: ${glFormat})`,
216
+ );
217
+ }
218
+
219
+ const glInternalFormat = view.getUint32(28, littleEndian);
220
+ if (blockInfoMap[glInternalFormat] === undefined) {
221
+ throw new Error(
222
+ `Unsupported KTX compressed texture format: 0x${glInternalFormat.toString(
223
+ 16,
224
+ )}`,
225
+ );
226
+ }
227
+
228
+ const width = view.getUint32(36, littleEndian);
229
+ const height = view.getUint32(40, littleEndian);
230
+ if (width === 0 || height === 0) {
231
+ throw new Error(`Invalid KTX texture dimensions: ${width}x${height}`);
232
+ }
233
+
234
+ const mipmapLevels = view.getUint32(56, littleEndian);
235
+ if (mipmapLevels === 0) {
236
+ throw new Error('KTX texture has no mipmap levels');
237
+ }
238
+
239
+ const bytesOfKeyValueData = view.getUint32(60, littleEndian);
240
+ const mipmaps: ArrayBuffer[] = [];
241
+ const buffer = view.buffer as ArrayBuffer;
242
+ let offset = 64 + bytesOfKeyValueData;
243
+
244
+ if (offset > view.byteLength) {
245
+ throw new Error('Invalid KTX file: key/value data exceeds file size');
246
+ }
247
+
248
+ for (let i = 0; i < mipmapLevels; i++) {
249
+ const imageSize = view.getUint32(offset, littleEndian);
250
+ offset += 4;
251
+
252
+ const end = offset + imageSize;
253
+
254
+ mipmaps.push(buffer.slice(offset, end));
255
+ offset = end;
256
+ if (offset % 4 !== 0) {
257
+ offset += 4 - (offset % 4);
258
+ }
259
+ }
260
+
261
+ return {
262
+ data: {
263
+ blockInfo: blockInfoMap[glInternalFormat]!,
264
+ glInternalFormat: glInternalFormat,
265
+ mipmaps,
266
+ width: width,
267
+ height: height,
268
+ type: 'ktx',
269
+ },
270
+ premultiplyAlpha: false,
133
271
  };
272
+ };
273
+
274
+ const uploadKTX = function (
275
+ glw: WebGlContextWrapper,
276
+ texture: WebGLTexture,
277
+ data: CompressedData,
278
+ ) {
279
+ const { glInternalFormat, mipmaps, width, height, blockInfo } = data;
280
+
281
+ glw.bindTexture(texture);
282
+
283
+ const blockWidth = blockInfo.width;
284
+ const blockHeight = blockInfo.height;
285
+ let w = width;
286
+ let h = height;
134
287
 
135
- let offset = 0;
136
- let width = data.pixelWidth || 0;
137
- let height = data.pixelHeight || 0;
288
+ for (let i = 0; i < mipmaps.length; i++) {
289
+ let view = new Uint8Array(mipmaps[i]!);
290
+
291
+ const uploadW = Math.ceil(w / blockWidth) * blockWidth;
292
+ const uploadH = Math.ceil(h / blockHeight) * blockHeight;
293
+
294
+ const expectedBytes =
295
+ Math.ceil(w / blockWidth) * Math.ceil(h / blockHeight) * blockInfo.bytes;
296
+
297
+ if (view.byteLength < expectedBytes) {
298
+ const padded = new Uint8Array(expectedBytes);
299
+ padded.set(view);
300
+ view = padded;
301
+ }
302
+
303
+ glw.compressedTexImage2D(i, glInternalFormat, uploadW, uploadH, 0, view);
304
+
305
+ w = Math.max(1, w >> 1);
306
+ h = Math.max(1, h >> 1);
307
+ }
138
308
 
139
- for (let i = 0; i < data.numberOfMipmapLevels; i++) {
140
- const level = ((width + 3) >> 2) * ((height + 3) >> 2) * 8;
141
- const view = new Uint8Array(
142
- arrayBuffer,
143
- pvrtcData.byteOffset + offset,
144
- level,
309
+ glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
310
+ glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
311
+ glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
312
+ glw.texParameteri(
313
+ glw.TEXTURE_MIN_FILTER,
314
+ mipmaps.length > 1 ? glw.LINEAR_MIPMAP_LINEAR : glw.LINEAR,
315
+ );
316
+ };
317
+
318
+ function pvrtcMipSize(width: number, height: number, bpp: 2 | 4) {
319
+ const minW = bpp === 2 ? 16 : 8;
320
+ const minH = 8;
321
+ const w = Math.max(width, minW);
322
+ const h = Math.max(height, minH);
323
+ return (w * h * bpp) / 8;
324
+ }
325
+
326
+ const loadPVR = async function (view: DataView): Promise<TextureData> {
327
+ const pixelFormatLow = view.getUint32(8, true);
328
+ const internalFormat = PVR_TO_GL_INTERNAL_FORMAT[pixelFormatLow];
329
+
330
+ if (internalFormat === undefined) {
331
+ throw new Error(
332
+ `Unsupported PVR pixel format: 0x${pixelFormatLow.toString(16)}`,
145
333
  );
334
+ }
335
+
336
+ const height = view.getInt32(24, true);
337
+ const width = view.getInt32(28, true);
146
338
 
147
- mipmaps.push(view);
148
- offset += level;
149
- width = width >> 1;
150
- height = height >> 1;
339
+ // validate dimensions
340
+ if (width === 0 || height === 0) {
341
+ throw new Error(`Invalid PVR texture dimensions: ${width}x${height}`);
342
+ }
343
+ const mipmapLevels = view.getInt32(44, true);
344
+ const metadataSize = view.getUint32(48, true);
345
+ const buffer = view.buffer as ArrayBuffer;
346
+
347
+ let offset = 52 + metadataSize;
348
+ if (offset > buffer.byteLength) {
349
+ throw new Error('Invalid PVR file: metadata exceeds file size');
350
+ }
351
+
352
+ const mipmaps: ArrayBuffer[] = [];
353
+
354
+ const block = blockInfoMap[internalFormat]!;
355
+
356
+ for (let i = 0; i < mipmapLevels; i++) {
357
+ const declaredSize = view.getUint32(offset, true);
358
+ const max = buffer.byteLength - (offset + 4);
359
+
360
+ if (declaredSize > 0 && declaredSize <= max) {
361
+ offset += 4;
362
+ const start = offset;
363
+ const end = offset + declaredSize;
364
+
365
+ mipmaps.push(buffer.slice(start, end));
366
+ offset = end;
367
+ offset = (offset + 3) & ~3; // align to 4 bytes
368
+ continue;
369
+ }
370
+
371
+ if (
372
+ pixelFormatLow === 0 ||
373
+ pixelFormatLow === 1 ||
374
+ pixelFormatLow === 2 ||
375
+ pixelFormatLow === 3
376
+ ) {
377
+ const bpp = pixelFormatLow === 0 || pixelFormatLow === 1 ? 2 : 4;
378
+ const computed = pvrtcMipSize(width >> i, height >> i, bpp);
379
+
380
+ mipmaps.push(buffer.slice(offset, offset + computed));
381
+ offset += computed;
382
+ offset = (offset + 3) & ~3; // align to 4 bytes
383
+ continue;
384
+ }
385
+
386
+ if (block !== undefined) {
387
+ const blockW = Math.ceil((width >> i) / block.width);
388
+ const blockH = Math.ceil((height >> i) / block.height);
389
+ const computed = blockW * blockH * block.bytes;
390
+
391
+ mipmaps.push(buffer.slice(offset, offset + computed));
392
+ offset += computed;
393
+ offset = (offset + 3) & ~3;
394
+ }
151
395
  }
152
396
 
153
397
  return {
154
398
  data: {
155
- glInternalFormat: pvrFormatEtc1,
156
- mipmaps: mipmaps,
157
- width: data.pixelWidth || 0,
158
- height: data.pixelHeight || 0,
399
+ blockInfo: blockInfoMap[internalFormat]!,
400
+ glInternalFormat: internalFormat,
401
+ mipmaps,
402
+ width: width,
403
+ height: height,
159
404
  type: 'pvr',
160
405
  },
161
406
  premultiplyAlpha: false,
162
407
  };
163
408
  };
409
+
410
+ const uploadPVR = function (
411
+ glw: WebGlContextWrapper,
412
+ texture: WebGLTexture,
413
+ data: CompressedData,
414
+ ) {
415
+ const { glInternalFormat, mipmaps, width, height } = data;
416
+
417
+ glw.bindTexture(texture);
418
+
419
+ let w = width;
420
+ let h = height;
421
+
422
+ for (let i = 0; i < mipmaps.length; i++) {
423
+ glw.compressedTexImage2D(
424
+ i,
425
+ glInternalFormat,
426
+ w,
427
+ h,
428
+ 0,
429
+ new Uint8Array(mipmaps[i]!),
430
+ );
431
+
432
+ w = Math.max(1, w >> 1);
433
+ h = Math.max(1, h >> 1);
434
+ }
435
+
436
+ glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
437
+ glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
438
+ glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
439
+ glw.texParameteri(
440
+ glw.TEXTURE_MIN_FILTER,
441
+ mipmaps.length > 1 ? glw.LINEAR_MIPMAP_LINEAR : glw.LINEAR,
442
+ );
443
+ };
444
+
445
+ type BlockInfo = {
446
+ width: number;
447
+ height: number;
448
+ bytes: number;
449
+ };
450
+
451
+ // Predefined block info for common compressed texture formats
452
+ const BLOCK_4x4x8: BlockInfo = { width: 4, height: 4, bytes: 8 };
453
+ const BLOCK_4x4x16: BlockInfo = { width: 4, height: 4, bytes: 16 };
454
+ const BLOCK_5x5x16: BlockInfo = { width: 5, height: 5, bytes: 16 };
455
+ const BLOCK_6x6x16: BlockInfo = { width: 6, height: 6, bytes: 16 };
456
+ const BLOCK_8x4x8: BlockInfo = { width: 8, height: 4, bytes: 8 };
457
+ const BLOCK_8x8x16: BlockInfo = { width: 8, height: 8, bytes: 16 };
458
+ const BLOCK_10x10x16: BlockInfo = { width: 10, height: 10, bytes: 16 };
459
+ const BLOCK_12x12x16: BlockInfo = { width: 12, height: 12, bytes: 16 };
460
+
461
+ // Map of GL internal formats to their corresponding block info
462
+ export const blockInfoMap: { [key: number]: BlockInfo } = {
463
+ // S3TC / DXTn (WEBGL_compressed_texture_s3tc, sRGB variants)
464
+ 0x83f0: BLOCK_4x4x8, // COMPRESSED_RGB_S3TC_DXT1_EXT
465
+ 0x83f1: BLOCK_4x4x8, // COMPRESSED_RGBA_S3TC_DXT1_EXT
466
+ 0x83f2: BLOCK_4x4x16, // COMPRESSED_RGBA_S3TC_DXT3_EXT
467
+ 0x83f3: BLOCK_4x4x16, // COMPRESSED_RGBA_S3TC_DXT5_EXT
468
+
469
+ // ETC1 / ETC2 / EAC
470
+ 0x8d64: BLOCK_4x4x8, // COMPRESSED_RGB_ETC1_WEBGL
471
+ 0x9274: BLOCK_4x4x8, // COMPRESSED_RGB8_ETC2
472
+ 0x9275: BLOCK_4x4x8, // COMPRESSED_SRGB8_ETC2
473
+ 0x9278: BLOCK_4x4x16, // COMPRESSED_RGBA8_ETC2_EAC
474
+ 0x9279: BLOCK_4x4x16, // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC
475
+
476
+ // PVRTC (WEBGL_compressed_texture_pvrtc)
477
+ 0x8c00: BLOCK_4x4x8, // COMPRESSED_RGB_PVRTC_4BPPV1_IMG
478
+ 0x8c02: BLOCK_4x4x8, // COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
479
+ 0x8c01: BLOCK_8x4x8, // COMPRESSED_RGB_PVRTC_2BPPV1_IMG
480
+ 0x8c03: BLOCK_8x4x8,
481
+
482
+ // ASTC (WEBGL_compressed_texture_astc)
483
+ 0x93b0: BLOCK_4x4x16, // COMPRESSED_RGBA_ASTC_4x4_KHR
484
+ 0x93d0: BLOCK_4x4x16, // COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR
485
+ 0x93b1: BLOCK_5x5x16, // 5x5
486
+ 0x93d1: BLOCK_5x5x16,
487
+ 0x93b2: BLOCK_6x6x16, // 6x6
488
+ 0x93d2: BLOCK_6x6x16,
489
+ 0x93b3: BLOCK_8x8x16, // 8x8
490
+ 0x93d3: BLOCK_8x8x16,
491
+ 0x93b4: BLOCK_10x10x16, // 10x10
492
+ 0x93d4: BLOCK_10x10x16,
493
+ 0x93b5: BLOCK_12x12x16, // 12x12
494
+ 0x93d5: BLOCK_12x12x16,
495
+ };
496
+
497
+ export const uploadCompressedTexture: Record<
498
+ string,
499
+ UploadCompressedTextureFunction
500
+ > = {
501
+ ktx: uploadKTX,
502
+ pvr: uploadPVR,
503
+ astc: uploadASTC,
504
+ };