utils.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import {
  2. Accessor,
  3. Document,
  4. type GLTF,
  5. Primitive,
  6. type Property,
  7. PropertyType,
  8. type Texture,
  9. type Transform,
  10. type TransformContext,
  11. type vec2,
  12. } from '@gltf-transform/core';
  13. import type { NdArray } from 'ndarray';
  14. import { getPixels, savePixels } from 'ndarray-pixels';
  15. const { POINTS, LINES, LINE_STRIP, LINE_LOOP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN } = Primitive.Mode;
  16. /**
  17. * Prepares a function used in an {@link Document#transform} pipeline. Use of this wrapper is
  18. * optional, and plain functions may be used in transform pipelines just as well. The wrapper is
  19. * used internally so earlier pipeline stages can detect and optimize based on later stages.
  20. * @hidden
  21. */
  22. export function createTransform(name: string, fn: Transform): Transform {
  23. Object.defineProperty(fn, 'name', { value: name });
  24. return fn;
  25. }
  26. /** @hidden */
  27. export function isTransformPending(context: TransformContext | undefined, initial: string, pending: string): boolean {
  28. if (!context) return false;
  29. const initialIndex = context.stack.lastIndexOf(initial);
  30. const pendingIndex = context.stack.lastIndexOf(pending);
  31. return initialIndex < pendingIndex;
  32. }
  33. /**
  34. * Performs a shallow merge on an 'options' object and a 'defaults' object.
  35. * Equivalent to `{...defaults, ...options}` _except_ that `undefined` values
  36. * in the 'options' object are ignored.
  37. *
  38. * @hidden
  39. */
  40. export function assignDefaults<Defaults, Options>(defaults: Defaults, options: Options): Defaults & Options {
  41. const result = { ...defaults } as Defaults & Partial<Options>;
  42. for (const key in options) {
  43. if (options[key] !== undefined) {
  44. // biome-ignore lint/suspicious/noExplicitAny: TODO
  45. result[key] = options[key] as any;
  46. }
  47. }
  48. return result as Defaults & Options;
  49. }
  50. /**
  51. * Maps pixels from source to target textures, with a per-pixel callback.
  52. * @hidden
  53. */
  54. export async function rewriteTexture(
  55. source: Texture,
  56. target: Texture,
  57. fn: (pixels: NdArray, i: number, j: number) => void,
  58. ): Promise<Texture | null> {
  59. if (!source) return null;
  60. const srcImage = source.getImage();
  61. if (!srcImage) return null;
  62. const pixels = await getPixels(srcImage, source.getMimeType());
  63. for (let i = 0; i < pixels.shape[0]; ++i) {
  64. for (let j = 0; j < pixels.shape[1]; ++j) {
  65. fn(pixels, i, j);
  66. }
  67. }
  68. const dstImage = await savePixels(pixels, 'image/png');
  69. return target.setImage(dstImage).setMimeType('image/png');
  70. }
  71. /** @hidden */
  72. export function getGLPrimitiveCount(prim: Primitive): number {
  73. const indices = prim.getIndices();
  74. const position = prim.getAttribute('POSITION')!;
  75. // Reference: https://www.khronos.org/opengl/wiki/Primitive
  76. switch (prim.getMode()) {
  77. case Primitive.Mode.POINTS:
  78. return indices ? indices.getCount() : position.getCount();
  79. case Primitive.Mode.LINES:
  80. return indices ? indices.getCount() / 2 : position.getCount() / 2;
  81. case Primitive.Mode.LINE_LOOP:
  82. return indices ? indices.getCount() : position.getCount();
  83. case Primitive.Mode.LINE_STRIP:
  84. return indices ? indices.getCount() - 1 : position.getCount() - 1;
  85. case Primitive.Mode.TRIANGLES:
  86. return indices ? indices.getCount() / 3 : position.getCount() / 3;
  87. case Primitive.Mode.TRIANGLE_STRIP:
  88. case Primitive.Mode.TRIANGLE_FAN:
  89. return indices ? indices.getCount() - 2 : position.getCount() - 2;
  90. default:
  91. throw new Error('Unexpected mode: ' + prim.getMode());
  92. }
  93. }
  94. /** @hidden */
  95. export class SetMap<K, V> {
  96. private _map = new Map<K, Set<V>>();
  97. public get size(): number {
  98. return this._map.size;
  99. }
  100. public has(k: K): boolean {
  101. return this._map.has(k);
  102. }
  103. public add(k: K, v: V): this {
  104. let entry = this._map.get(k);
  105. if (!entry) {
  106. entry = new Set();
  107. this._map.set(k, entry);
  108. }
  109. entry.add(v);
  110. return this;
  111. }
  112. public get(k: K): Set<V> {
  113. return this._map.get(k) || new Set();
  114. }
  115. public keys(): Iterable<K> {
  116. return this._map.keys();
  117. }
  118. }
  119. /** @hidden */
  120. export function formatBytes(bytes: number, decimals = 2): string {
  121. if (bytes === 0) return '0 Bytes';
  122. const k = 1000;
  123. const dm = decimals < 0 ? 0 : decimals;
  124. const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  125. const i = Math.floor(Math.log(bytes) / Math.log(k));
  126. return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  127. }
  128. const _longFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
  129. /** @hidden */
  130. export function formatLong(x: number): string {
  131. return _longFormatter.format(x);
  132. }
  133. /** @hidden */
  134. export function formatDelta(a: number, b: number, decimals = 2): string {
  135. const prefix = a > b ? '–' : '+';
  136. const suffix = '%';
  137. return prefix + ((Math.abs(a - b) / a) * 100).toFixed(decimals) + suffix;
  138. }
  139. /** @hidden */
  140. export function formatDeltaOp(a: number, b: number) {
  141. return `${formatLong(a)} → ${formatLong(b)} (${formatDelta(a, b)})`;
  142. }
  143. /**
  144. * Returns a list of all unique vertex attributes on the given primitive and
  145. * its morph targets.
  146. * @hidden
  147. */
  148. export function deepListAttributes(prim: Primitive): Accessor[] {
  149. const accessors: Accessor[] = [];
  150. for (const attribute of prim.listAttributes()) {
  151. accessors.push(attribute);
  152. }
  153. for (const target of prim.listTargets()) {
  154. for (const attribute of target.listAttributes()) {
  155. accessors.push(attribute);
  156. }
  157. }
  158. return Array.from(new Set(accessors));
  159. }
  160. /** @hidden */
  161. export function deepSwapAttribute(prim: Primitive, src: Accessor, dst: Accessor): void {
  162. prim.swap(src, dst);
  163. for (const target of prim.listTargets()) {
  164. target.swap(src, dst);
  165. }
  166. }
  167. /**
  168. * Disposes of a {@link Primitive} and any {@link Accessor Accessors} for which
  169. * it is the last remaining parent.
  170. * @hidden
  171. */
  172. export function deepDisposePrimitive(prim: Primitive): void {
  173. const indices = prim.getIndices();
  174. const attributes = deepListAttributes(prim);
  175. prim.dispose();
  176. if (indices && !isUsed(indices)) {
  177. indices.dispose();
  178. }
  179. for (const attribute of attributes) {
  180. if (!isUsed(attribute)) {
  181. attribute.dispose();
  182. }
  183. }
  184. }
  185. /** @hidden */
  186. export function shallowEqualsArray(a: ArrayLike<unknown> | null, b: ArrayLike<unknown> | null): boolean {
  187. if (a == null && b == null) return true;
  188. if (a == null || b == null) return false;
  189. if (a.length !== b.length) return false;
  190. for (let i = 0; i < a.length; i++) {
  191. if (a[i] !== b[i]) return false;
  192. }
  193. return true;
  194. }
  195. /** Clones an {@link Accessor} without creating a copy of its underlying TypedArray data. */
  196. export function shallowCloneAccessor(document: Document, accessor: Accessor): Accessor {
  197. return document
  198. .createAccessor(accessor.getName())
  199. .setArray(accessor.getArray())
  200. .setType(accessor.getType())
  201. .setBuffer(accessor.getBuffer())
  202. .setNormalized(accessor.getNormalized())
  203. .setSparse(accessor.getSparse());
  204. }
  205. /** @hidden */
  206. export function createIndices(
  207. count: number,
  208. maxIndex: number = count,
  209. ): Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer> {
  210. const array = createIndicesEmpty(count, maxIndex);
  211. for (let i = 0; i < array.length; i++) array[i] = i;
  212. return array;
  213. }
  214. /** @hidden */
  215. export function createIndicesEmpty(
  216. count: number,
  217. maxIndex: number = count,
  218. ): Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer> {
  219. return maxIndex <= 65534 ? new Uint16Array(count) : new Uint32Array(count);
  220. }
  221. /** @hidden */
  222. export function isUsed(prop: Property): boolean {
  223. return prop.listParents().some((parent) => parent.propertyType !== PropertyType.ROOT);
  224. }
  225. /** @hidden */
  226. export function isEmptyObject(object: Record<string, unknown>): boolean {
  227. for (const _key in object) return false;
  228. return true;
  229. }
  230. /**
  231. * Creates a unique key associated with the structure and draw call characteristics of
  232. * a {@link Primitive}, independent of its vertex content. Helper method, used to
  233. * identify candidate Primitives for joining.
  234. * @hidden
  235. */
  236. export function createPrimGroupKey(prim: Primitive): string {
  237. const document = Document.fromGraph(prim.getGraph())!;
  238. const material = prim.getMaterial();
  239. const materialIndex = document.getRoot().listMaterials().indexOf(material!);
  240. const mode = prim.getMode();
  241. const indices = !!prim.getIndices();
  242. const attributes = prim
  243. .listSemantics()
  244. .sort()
  245. .map((semantic) => {
  246. const attribute = prim.getAttribute(semantic)!;
  247. const elementSize = attribute.getElementSize();
  248. const componentType = attribute.getComponentType();
  249. return `${semantic}:${elementSize}:${componentType}`;
  250. })
  251. .join('+');
  252. const targets = prim
  253. .listTargets()
  254. .map((target) => {
  255. return target
  256. .listSemantics()
  257. .sort()
  258. .map((semantic) => {
  259. const attribute = prim.getAttribute(semantic)!;
  260. const elementSize = attribute.getElementSize();
  261. const componentType = attribute.getComponentType();
  262. return `${semantic}:${elementSize}:${componentType}`;
  263. })
  264. .join('+');
  265. })
  266. .join('~');
  267. return `${materialIndex}|${mode}|${indices}|${attributes}|${targets}`;
  268. }
  269. /**
  270. * Scales `size` NxN dimensions to fit within `limit` NxN dimensions, without
  271. * changing aspect ratio. If `size` <= `limit` in all dimensions, returns `size`.
  272. * @hidden
  273. */
  274. export function fitWithin(size: vec2, limit: vec2): vec2 {
  275. const [maxWidth, maxHeight] = limit;
  276. const [srcWidth, srcHeight] = size;
  277. if (srcWidth <= maxWidth && srcHeight <= maxHeight) return size;
  278. let dstWidth = srcWidth;
  279. let dstHeight = srcHeight;
  280. if (dstWidth > maxWidth) {
  281. dstHeight = Math.floor(dstHeight * (maxWidth / dstWidth));
  282. dstWidth = maxWidth;
  283. }
  284. if (dstHeight > maxHeight) {
  285. dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight));
  286. dstHeight = maxHeight;
  287. }
  288. return [dstWidth, dstHeight];
  289. }
  290. type ResizePreset = 'nearest-pot' | 'ceil-pot' | 'floor-pot';
  291. /**
  292. * Scales `size` NxN dimensions to the specified power of two.
  293. * @hidden
  294. */
  295. export function fitPowerOfTwo(size: vec2, method: ResizePreset): vec2 {
  296. if (isPowerOfTwo(size[0]) && isPowerOfTwo(size[1])) {
  297. return size;
  298. }
  299. switch (method) {
  300. case 'nearest-pot':
  301. return size.map(nearestPowerOfTwo) as vec2;
  302. case 'ceil-pot':
  303. return size.map(ceilPowerOfTwo) as vec2;
  304. case 'floor-pot':
  305. return size.map(floorPowerOfTwo) as vec2;
  306. }
  307. }
  308. function isPowerOfTwo(value: number): boolean {
  309. if (value <= 2) return true;
  310. return (value & (value - 1)) === 0 && value !== 0;
  311. }
  312. function nearestPowerOfTwo(value: number): number {
  313. if (value <= 4) return 4;
  314. const lo = floorPowerOfTwo(value);
  315. const hi = ceilPowerOfTwo(value);
  316. if (hi - value > value - lo) return lo;
  317. return hi;
  318. }
  319. export function floorPowerOfTwo(value: number): number {
  320. return Math.pow(2, Math.floor(Math.log(value) / Math.LN2));
  321. }
  322. export function ceilPowerOfTwo(value: number): number {
  323. return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2));
  324. }
  325. /**
  326. * Mapping from any glTF primitive mode to its equivalent basic mode, as returned by
  327. * {@link convertPrimitiveMode}.
  328. * @hidden
  329. */
  330. export const BASIC_MODE_MAPPING = {
  331. [POINTS]: POINTS,
  332. [LINES]: LINES,
  333. [LINE_STRIP]: LINES,
  334. [LINE_LOOP]: LINES,
  335. [TRIANGLES]: TRIANGLES,
  336. [TRIANGLE_STRIP]: TRIANGLES,
  337. [TRIANGLE_FAN]: TRIANGLES,
  338. } as Record<GLTF.MeshPrimitiveMode, GLTF.MeshPrimitiveMode>;
  339. /**
  340. * Whether the primitive mode supports KHR_mesh_primitive_restart.
  341. * @hidden
  342. * @internal
  343. */
  344. export function isPrimitiveRestartMode(mode: GLTF.MeshPrimitiveMode): boolean {
  345. return mode === LINE_STRIP || mode === LINE_LOOP || mode === TRIANGLE_STRIP || mode === TRIANGLE_FAN;
  346. }
  347. /**
  348. * Returns the applicable primitive restart value (see KHR_mesh_primitive_restart)
  349. * for the given index accessor.
  350. * @hidden
  351. * @internal
  352. */
  353. export function getPrimitiveRestartIndex(componentType: GLTF.AccessorComponentType): number {
  354. switch (componentType) {
  355. case Accessor.ComponentType.UNSIGNED_INT:
  356. return 0xffffffff;
  357. case Accessor.ComponentType.UNSIGNED_SHORT:
  358. return 0xffff;
  359. case Accessor.ComponentType.UNSIGNED_BYTE:
  360. return 0xff;
  361. default:
  362. return -1;
  363. }
  364. }