@flighthq/texture 0.1.0
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.
- package/dist/cubeTexture.d.ts +9 -0
- package/dist/cubeTexture.d.ts.map +1 -0
- package/dist/cubeTexture.js +83 -0
- package/dist/cubeTexture.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/sampler.d.ts +10 -0
- package/dist/sampler.d.ts.map +1 -0
- package/dist/sampler.js +71 -0
- package/dist/sampler.js.map +1 -0
- package/dist/texture.d.ts +17 -0
- package/dist/texture.d.ts.map +1 -0
- package/dist/texture.js +151 -0
- package/dist/texture.js.map +1 -0
- package/package.json +39 -0
- package/src/cubeTexture.test.ts +174 -0
- package/src/sampler.test.ts +144 -0
- package/src/texture.test.ts +410 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CubeTexture, CubeTextureLike, ImageResource } from '@flighthq/types';
|
|
2
|
+
export declare function cloneCubeTexture(source: Readonly<CubeTextureLike>): CubeTexture;
|
|
3
|
+
export declare function copyCubeTexture(out: CubeTextureLike, source: Readonly<CubeTextureLike>): void;
|
|
4
|
+
export declare function createCubeTexture(opts?: Readonly<Partial<CubeTextureLike>>): CubeTexture;
|
|
5
|
+
export declare function equalsCubeTexture(a: Readonly<CubeTextureLike> | null | undefined, b: Readonly<CubeTextureLike> | null | undefined): boolean;
|
|
6
|
+
export declare function getCubeTextureFaceSize(cube: Readonly<CubeTextureLike>): number;
|
|
7
|
+
export declare function isCubeTextureComplete(cube: Readonly<CubeTextureLike>): boolean;
|
|
8
|
+
export declare function setCubeTextureFace(cube: CubeTextureLike, faceIndex: number, image: ImageResource | null): void;
|
|
9
|
+
//# sourceMappingURL=cubeTexture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cubeTexture.d.ts","sourceRoot":"","sources":["../src/cubeTexture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOnF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,WAAW,CAM/E;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,IAAI,CAiB7F;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG,WAAW,CAMxF;AAID,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,IAAI,GAAG,SAAS,EAC/C,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,IAAI,GAAG,SAAS,GAC9C,OAAO,CAST;AAID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,MAAM,CAM9E;AAID,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,OAAO,CAE9E;AAMD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAE9G"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
import { cloneSampler, copySampler, createSampler, equalsSampler } from './sampler';
|
|
3
|
+
// Allocates an independent CubeTexture over the SAME six face pixels: each ImageResource reference
|
|
4
|
+
// is shared (the faces array itself is a fresh copy, so reassigning a face on the clone does not
|
|
5
|
+
// touch the source), while the Sampler is deep-cloned so the cubes sample independently.
|
|
6
|
+
export function cloneCubeTexture(source) {
|
|
7
|
+
return createEntity({
|
|
8
|
+
colorSpace: source.colorSpace,
|
|
9
|
+
faces: source.faces.slice(),
|
|
10
|
+
sampler: cloneSampler(source.sampler),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
// Copies every CubeTexture field from source into out in place. Each face reference is shared;
|
|
14
|
+
// the Sampler is copied into out's existing Sampler entity (its identity is preserved). Safe when
|
|
15
|
+
// out aliases source: all input values are read into locals before any writes.
|
|
16
|
+
export function copyCubeTexture(out, source) {
|
|
17
|
+
const colorSpace = source.colorSpace;
|
|
18
|
+
const f0 = source.faces[0];
|
|
19
|
+
const f1 = source.faces[1];
|
|
20
|
+
const f2 = source.faces[2];
|
|
21
|
+
const f3 = source.faces[3];
|
|
22
|
+
const f4 = source.faces[4];
|
|
23
|
+
const f5 = source.faces[5];
|
|
24
|
+
copySampler(out.sampler, source.sampler);
|
|
25
|
+
out.colorSpace = colorSpace;
|
|
26
|
+
const faces = out.faces;
|
|
27
|
+
faces[0] = f0;
|
|
28
|
+
faces[1] = f1;
|
|
29
|
+
faces[2] = f2;
|
|
30
|
+
faces[3] = f3;
|
|
31
|
+
faces[4] = f4;
|
|
32
|
+
faces[5] = f5;
|
|
33
|
+
}
|
|
34
|
+
// Builds a CubeTexture: six unbound faces (all null, in the canonical +X, -X, +Y, -Y, +Z, -Z
|
|
35
|
+
// order), a default Sampler, and 'srgb' color space (the environment-radiance default). Pass
|
|
36
|
+
// CubeTextureLike fields to override any of these; a supplied `faces` array is copied.
|
|
37
|
+
export function createCubeTexture(opts) {
|
|
38
|
+
return createEntity({
|
|
39
|
+
colorSpace: opts?.colorSpace ?? 'srgb',
|
|
40
|
+
faces: opts?.faces ? opts.faces.slice() : [null, null, null, null, null, null],
|
|
41
|
+
sampler: opts?.sampler ? cloneSampler(opts.sampler) : createSampler(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// True when both cube textures describe identical state: same color space, same sampler state, and
|
|
45
|
+
// the same face references in every slot. Returns false for null/undefined operands.
|
|
46
|
+
export function equalsCubeTexture(a, b) {
|
|
47
|
+
if (!a || !b)
|
|
48
|
+
return false;
|
|
49
|
+
if (a === b)
|
|
50
|
+
return true;
|
|
51
|
+
if (a.colorSpace !== b.colorSpace)
|
|
52
|
+
return false;
|
|
53
|
+
if (!equalsSampler(a.sampler, b.sampler))
|
|
54
|
+
return false;
|
|
55
|
+
for (let i = 0; i < 6; i++) {
|
|
56
|
+
if (a.faces[i] !== b.faces[i])
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// Returns the pixel size (width = height for a cube face) of the first non-null face, or -1 when
|
|
62
|
+
// no face is bound. Cube faces are assumed square; width is used as the canonical face size.
|
|
63
|
+
export function getCubeTextureFaceSize(cube) {
|
|
64
|
+
for (let i = 0; i < 6; i++) {
|
|
65
|
+
const face = cube.faces[i];
|
|
66
|
+
if (face !== null)
|
|
67
|
+
return face.width;
|
|
68
|
+
}
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
// True when all six faces are non-null. Materials and IBL pipelines (skybox, reflections) gate
|
|
72
|
+
// sampling behind this; an incomplete cube is treated as absent.
|
|
73
|
+
export function isCubeTextureComplete(cube) {
|
|
74
|
+
return cube.faces.every((face) => face !== null);
|
|
75
|
+
}
|
|
76
|
+
// Assigns a single face image in place. Use the CubeFace* constants from @flighthq/types
|
|
77
|
+
// (CubeFacePositiveX = 0, CubeFaceNegativeX = 1, CubeFacePositiveY = 2, CubeFaceNegativeY = 3,
|
|
78
|
+
// CubeFacePositiveZ = 4, CubeFaceNegativeZ = 5) instead of magic-number indices.
|
|
79
|
+
// Pass null to unbind the face.
|
|
80
|
+
export function setCubeTextureFace(cube, faceIndex, image) {
|
|
81
|
+
cube.faces[faceIndex] = image;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=cubeTexture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cubeTexture.js","sourceRoot":"","sources":["../src/cubeTexture.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Sampler, SamplerLike } from '@flighthq/types';
|
|
2
|
+
export declare function cloneSampler(source: Readonly<SamplerLike>): Sampler;
|
|
3
|
+
export declare function copySampler(out: SamplerLike, source: Readonly<SamplerLike>): void;
|
|
4
|
+
export declare function createAnisotropicSampler(level: number): Sampler;
|
|
5
|
+
export declare function createClampLinearSampler(): Sampler;
|
|
6
|
+
export declare function createPixelArtSampler(): Sampler;
|
|
7
|
+
export declare function createSampler(opts?: Readonly<Partial<SamplerLike>>): Sampler;
|
|
8
|
+
export declare function createTilingSampler(): Sampler;
|
|
9
|
+
export declare function equalsSampler(a: Readonly<SamplerLike> | null | undefined, b: Readonly<SamplerLike> | null | undefined): boolean;
|
|
10
|
+
//# sourceMappingURL=sampler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampler.d.ts","sourceRoot":"","sources":["../src/sampler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,OAAO,CASnE;AAID,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAOjF;AAKD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/D;AAID,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAID,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAKD,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,OAAO,CAS5E;AAID,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAID,wBAAgB,aAAa,CAC3B,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3C,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAWT"}
|
package/dist/sampler.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
// Allocates an independent Sampler with the same sampling state. Sampler holds only plain values,
|
|
3
|
+
// so the clone shares nothing mutable with its source.
|
|
4
|
+
export function cloneSampler(source) {
|
|
5
|
+
return createEntity({
|
|
6
|
+
anisotropy: source.anisotropy,
|
|
7
|
+
magFilter: source.magFilter,
|
|
8
|
+
minFilter: source.minFilter,
|
|
9
|
+
mipmaps: source.mipmaps,
|
|
10
|
+
wrapU: source.wrapU,
|
|
11
|
+
wrapV: source.wrapV,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// Copies every sampling field from source into out in place. Safe when out aliases source: all
|
|
15
|
+
// reads precede the writes implicitly because each field is read and written independently.
|
|
16
|
+
export function copySampler(out, source) {
|
|
17
|
+
out.anisotropy = source.anisotropy;
|
|
18
|
+
out.magFilter = source.magFilter;
|
|
19
|
+
out.minFilter = source.minFilter;
|
|
20
|
+
out.mipmaps = source.mipmaps;
|
|
21
|
+
out.wrapU = source.wrapU;
|
|
22
|
+
out.wrapV = source.wrapV;
|
|
23
|
+
}
|
|
24
|
+
// Anisotropic sampler for high-quality surface textures rendered at oblique angles. Uses the given
|
|
25
|
+
// anisotropy level (typically 4 or 16 — capped to hardware maximum by the backend), with
|
|
26
|
+
// trilinear filtering and clamp-to-edge. Pass level 1 to disable anisotropy.
|
|
27
|
+
export function createAnisotropicSampler(level) {
|
|
28
|
+
return createSampler({ anisotropy: level });
|
|
29
|
+
}
|
|
30
|
+
// Clamp-to-edge linear sampler — the default preset, named for explicit use in builder code and
|
|
31
|
+
// materials that want to signal their intent clearly. Clamp/linear/trilinear/mipmaps on.
|
|
32
|
+
export function createClampLinearSampler() {
|
|
33
|
+
return createSampler();
|
|
34
|
+
}
|
|
35
|
+
// Pixel-art sampler: nearest-neighbor filtering, clamp-to-edge, mipmaps disabled. Produces crisp,
|
|
36
|
+
// unsmoothed pixels for retro / pixel-art rendering styles.
|
|
37
|
+
export function createPixelArtSampler() {
|
|
38
|
+
return createSampler({ magFilter: 'nearest', minFilter: 'nearest', mipmaps: false });
|
|
39
|
+
}
|
|
40
|
+
// Builds a Sampler with the AAA-default sampling state: clamp-to-edge on both axes, linear
|
|
41
|
+
// magnification, trilinear minification, a generated mip chain, and anisotropy disabled (1). Pass
|
|
42
|
+
// SamplerLike fields to override any of these.
|
|
43
|
+
export function createSampler(opts) {
|
|
44
|
+
return createEntity({
|
|
45
|
+
anisotropy: opts?.anisotropy ?? 1,
|
|
46
|
+
magFilter: opts?.magFilter ?? 'linear',
|
|
47
|
+
minFilter: opts?.minFilter ?? 'linear-mipmap-linear',
|
|
48
|
+
mipmaps: opts?.mipmaps ?? true,
|
|
49
|
+
wrapU: opts?.wrapU ?? 'clamp-to-edge',
|
|
50
|
+
wrapV: opts?.wrapV ?? 'clamp-to-edge',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Tiling sampler: repeat wrap on both axes, trilinear filtering, mipmaps on. Suitable for
|
|
54
|
+
// seamless tiling surface textures (terrain, ground planes, repeating patterns).
|
|
55
|
+
export function createTilingSampler() {
|
|
56
|
+
return createSampler({ wrapU: 'repeat', wrapV: 'repeat' });
|
|
57
|
+
}
|
|
58
|
+
// True when both samplers describe identical sampling state. Returns false for null/undefined
|
|
59
|
+
// operands so callers can compare nullable references directly.
|
|
60
|
+
export function equalsSampler(a, b) {
|
|
61
|
+
if (!a || !b)
|
|
62
|
+
return false;
|
|
63
|
+
return (a === b ||
|
|
64
|
+
(a.anisotropy === b.anisotropy &&
|
|
65
|
+
a.magFilter === b.magFilter &&
|
|
66
|
+
a.minFilter === b.minFilter &&
|
|
67
|
+
a.mipmaps === b.mipmaps &&
|
|
68
|
+
a.wrapU === b.wrapU &&
|
|
69
|
+
a.wrapV === b.wrapV));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=sampler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampler.js","sourceRoot":"","sources":["../src/sampler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,kGAAkG;AAClG,uDAAuD;AACvD,MAAM,UAAU,YAAY,CAAC,MAA6B;IACxD,OAAO,YAAY,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC,CAAC;AACL,CAAC;AAED,+FAA+F;AAC/F,4FAA4F;AAC5F,MAAM,UAAU,WAAW,CAAC,GAAgB,EAAE,MAA6B;IACzE,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IACnC,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACjC,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACjC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC7B,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IACzB,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;AAC3B,CAAC;AAED,mGAAmG;AACnG,yFAAyF;AACzF,6EAA6E;AAC7E,MAAM,UAAU,wBAAwB,CAAC,KAAa;IACpD,OAAO,aAAa,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,gGAAgG;AAChG,yFAAyF;AACzF,MAAM,UAAU,wBAAwB;IACtC,OAAO,aAAa,EAAE,CAAC;AACzB,CAAC;AAED,kGAAkG;AAClG,4DAA4D;AAC5D,MAAM,UAAU,qBAAqB;IACnC,OAAO,aAAa,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;AACvF,CAAC;AAED,2FAA2F;AAC3F,kGAAkG;AAClG,+CAA+C;AAC/C,MAAM,UAAU,aAAa,CAAC,IAAqC;IACjE,OAAO,YAAY,CAAC;QAClB,UAAU,EAAE,IAAI,EAAE,UAAU,IAAI,CAAC;QACjC,SAAS,EAAE,IAAI,EAAE,SAAS,IAAI,QAAQ;QACtC,SAAS,EAAE,IAAI,EAAE,SAAS,IAAI,sBAAsB;QACpD,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,IAAI;QAC9B,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,eAAe;QACrC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,eAAe;KACtC,CAAC,CAAC;AACL,CAAC;AAED,0FAA0F;AAC1F,iFAAiF;AACjF,MAAM,UAAU,mBAAmB;IACjC,OAAO,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED,8FAA8F;AAC9F,gEAAgE;AAChE,MAAM,UAAU,aAAa,CAC3B,CAA2C,EAC3C,CAA2C;IAE3C,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3B,OAAO,CACL,CAAC,KAAK,CAAC;QACP,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU;YAC5B,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS;YAC3B,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS;YAC3B,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;YACvB,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;YACnB,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,CACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ImageResource, Matrix3Like, Texture, TextureLike, Vector2Like } from '@flighthq/types';
|
|
2
|
+
export declare function cloneTexture(source: Readonly<TextureLike>): Texture;
|
|
3
|
+
export declare function copyTexture(out: TextureLike, source: Readonly<TextureLike>): void;
|
|
4
|
+
export declare function createTexture(opts?: Readonly<Partial<TextureLike>>): Texture;
|
|
5
|
+
export declare function equalsTexture(a: Readonly<TextureLike> | null | undefined, b: Readonly<TextureLike> | null | undefined): boolean;
|
|
6
|
+
export declare function getTextureHeight(texture: Readonly<TextureLike>): number;
|
|
7
|
+
export declare function getTextureInverseUvMatrix(out: Matrix3Like, texture: Readonly<TextureLike>): void;
|
|
8
|
+
export declare function getTextureUvMatrix(out: Matrix3Like, texture: Readonly<TextureLike>): void;
|
|
9
|
+
export declare function getTextureWidth(texture: Readonly<TextureLike>): number;
|
|
10
|
+
export declare function isTextureReady(texture: Readonly<TextureLike>): boolean;
|
|
11
|
+
export declare function resetTextureUvTransform(texture: TextureLike): void;
|
|
12
|
+
export declare function setTextureImage(texture: TextureLike, image: ImageResource | null): void;
|
|
13
|
+
export declare function setTextureUvOffset(texture: TextureLike, x: number, y: number): void;
|
|
14
|
+
export declare function setTextureUvRotation(texture: TextureLike, radians: number): void;
|
|
15
|
+
export declare function setTextureUvScale(texture: TextureLike, x: number, y: number): void;
|
|
16
|
+
export declare function transformTextureUv(out: Vector2Like, texture: Readonly<TextureLike>, u: number, v: number): void;
|
|
17
|
+
//# sourceMappingURL=texture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"texture.d.ts","sourceRoot":"","sources":["../src/texture.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAOrG,wBAAgB,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,OAAO,CASnE;AAKD,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAUjF;AAKD,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,OAAO,CAS5E;AAID,wBAAgB,aAAa,CAC3B,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3C,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAaT;AAGD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,MAAM,CAEvE;AAOD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAGhG;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAkBzF;AAGD,wBAAgB,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,MAAM,CAEtE;AAID,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,OAAO,CAEtE;AAID,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAMlE;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAEvF;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAGnF;AAGD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhF;AAGD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAGlF;AAMD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAU/G"}
|
package/dist/texture.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createEntity } from '@flighthq/entity';
|
|
2
|
+
import { cloneVector2, copyVector2, createVector2, inverseMatrix3 } from '@flighthq/geometry';
|
|
3
|
+
import { cloneSampler, copySampler, createSampler, equalsSampler } from './sampler';
|
|
4
|
+
// Allocates an independent Texture over the SAME image pixels: the ImageResource reference is
|
|
5
|
+
// shared (clone the resource separately to upload into a second render state), while the Sampler
|
|
6
|
+
// and the uv-transform vectors are deep-cloned so the two textures can be sampled independently.
|
|
7
|
+
export function cloneTexture(source) {
|
|
8
|
+
return createEntity({
|
|
9
|
+
colorSpace: source.colorSpace,
|
|
10
|
+
image: source.image,
|
|
11
|
+
sampler: cloneSampler(source.sampler),
|
|
12
|
+
uvOffset: cloneVector2(source.uvOffset),
|
|
13
|
+
uvRotation: source.uvRotation,
|
|
14
|
+
uvScale: cloneVector2(source.uvScale),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// Copies every Texture field from source into out in place. The image reference is shared; the
|
|
18
|
+
// Sampler and uv-transform vectors are copied into out's existing entities (their identities are
|
|
19
|
+
// preserved). Safe when out aliases source.
|
|
20
|
+
export function copyTexture(out, source) {
|
|
21
|
+
const colorSpace = source.colorSpace;
|
|
22
|
+
const image = source.image;
|
|
23
|
+
const uvRotation = source.uvRotation;
|
|
24
|
+
copySampler(out.sampler, source.sampler);
|
|
25
|
+
copyVector2(out.uvOffset, source.uvOffset);
|
|
26
|
+
copyVector2(out.uvScale, source.uvScale);
|
|
27
|
+
out.colorSpace = colorSpace;
|
|
28
|
+
out.image = image;
|
|
29
|
+
out.uvRotation = uvRotation;
|
|
30
|
+
}
|
|
31
|
+
// Builds a Texture: an unbound image slot (null), a default Sampler, 'srgb' color space (the
|
|
32
|
+
// albedo default — data maps override to 'linear'), and an identity KHR_texture_transform
|
|
33
|
+
// (zero offset, unit scale, no rotation). Pass TextureLike fields to override any of these.
|
|
34
|
+
export function createTexture(opts) {
|
|
35
|
+
return createEntity({
|
|
36
|
+
colorSpace: opts?.colorSpace ?? 'srgb',
|
|
37
|
+
image: opts?.image ?? null,
|
|
38
|
+
sampler: opts?.sampler ? cloneSampler(opts.sampler) : createSampler(),
|
|
39
|
+
uvOffset: opts?.uvOffset ? cloneVector2(opts.uvOffset) : createVector2(0, 0),
|
|
40
|
+
uvRotation: opts?.uvRotation ?? 0,
|
|
41
|
+
uvScale: opts?.uvScale ? cloneVector2(opts.uvScale) : createVector2(1, 1),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// True when both textures describe identical state: same color space, same sampler state, the same
|
|
45
|
+
// image reference, and the same uv-transform values. Returns false for null/undefined operands.
|
|
46
|
+
export function equalsTexture(a, b) {
|
|
47
|
+
if (!a || !b)
|
|
48
|
+
return false;
|
|
49
|
+
if (a === b)
|
|
50
|
+
return true;
|
|
51
|
+
return (a.colorSpace === b.colorSpace &&
|
|
52
|
+
a.image === b.image &&
|
|
53
|
+
a.uvRotation === b.uvRotation &&
|
|
54
|
+
a.uvOffset.x === b.uvOffset.x &&
|
|
55
|
+
a.uvOffset.y === b.uvOffset.y &&
|
|
56
|
+
a.uvScale.x === b.uvScale.x &&
|
|
57
|
+
a.uvScale.y === b.uvScale.y &&
|
|
58
|
+
equalsSampler(a.sampler, b.sampler));
|
|
59
|
+
}
|
|
60
|
+
// Returns the pixel height of the texture's bound image, or -1 when no image is bound.
|
|
61
|
+
export function getTextureHeight(texture) {
|
|
62
|
+
return texture.image !== null ? texture.image.height : -1;
|
|
63
|
+
}
|
|
64
|
+
// Composes the KHR_texture_transform fields and inverts the result, producing the matrix that maps
|
|
65
|
+
// already-transformed uv coordinates back to the unit-square source uv. The forward transform is
|
|
66
|
+
// affine, so the inverse always exists for a non-degenerate (non-zero) scale; a zero scale yields a
|
|
67
|
+
// degenerate inverse (matching inverseMatrix3's affine-singular handling).
|
|
68
|
+
// Out-param form — write into a pre-allocated Matrix3 to avoid per-call allocation.
|
|
69
|
+
export function getTextureInverseUvMatrix(out, texture) {
|
|
70
|
+
getTextureUvMatrix(out, texture);
|
|
71
|
+
inverseMatrix3(out, out);
|
|
72
|
+
}
|
|
73
|
+
// Composes the KHR_texture_transform fields (uvOffset, uvRotation, uvScale) into the 3×3 matrix
|
|
74
|
+
// a shader consumes at sample time. Row-major layout matching @flighthq/geometry Matrix3.
|
|
75
|
+
// The resulting transform applies: scale → rotate → translate, per the KHR_texture_transform spec:
|
|
76
|
+
// row 0 = [sx*cos(r), -sy*sin(r), tx]; row 1 = [sx*sin(r), sy*cos(r), ty]; row 2 = [0, 0, 1].
|
|
77
|
+
// Out-param form — write result into a pre-allocated Matrix3 to avoid per-call allocation.
|
|
78
|
+
// Safe when out is an unrelated scratch; not intended for aliased input (no in-param here).
|
|
79
|
+
export function getTextureUvMatrix(out, texture) {
|
|
80
|
+
const r = texture.uvRotation;
|
|
81
|
+
const sx = texture.uvScale.x;
|
|
82
|
+
const sy = texture.uvScale.y;
|
|
83
|
+
const tx = texture.uvOffset.x;
|
|
84
|
+
const ty = texture.uvOffset.y;
|
|
85
|
+
const cosR = Math.cos(r);
|
|
86
|
+
const sinR = Math.sin(r);
|
|
87
|
+
const m = out.m;
|
|
88
|
+
m[0] = sx * cosR;
|
|
89
|
+
m[1] = -sy * sinR;
|
|
90
|
+
m[2] = tx;
|
|
91
|
+
m[3] = sx * sinR;
|
|
92
|
+
m[4] = sy * cosR;
|
|
93
|
+
m[5] = ty;
|
|
94
|
+
m[6] = 0;
|
|
95
|
+
m[7] = 0;
|
|
96
|
+
m[8] = 1;
|
|
97
|
+
}
|
|
98
|
+
// Returns the pixel width of the texture's bound image, or -1 when no image is bound.
|
|
99
|
+
export function getTextureWidth(texture) {
|
|
100
|
+
return texture.image !== null ? texture.image.width : -1;
|
|
101
|
+
}
|
|
102
|
+
// True once the texture references a pixel source. A texture with a null image is treated as an
|
|
103
|
+
// absent slot by materials, so this is the gate a material samples behind.
|
|
104
|
+
export function isTextureReady(texture) {
|
|
105
|
+
return texture.image !== null;
|
|
106
|
+
}
|
|
107
|
+
// Resets the KHR_texture_transform to identity in place: zero offset, no rotation, unit scale.
|
|
108
|
+
// Leaves the image, color space, and sampler untouched.
|
|
109
|
+
export function resetTextureUvTransform(texture) {
|
|
110
|
+
texture.uvOffset.x = 0;
|
|
111
|
+
texture.uvOffset.y = 0;
|
|
112
|
+
texture.uvRotation = 0;
|
|
113
|
+
texture.uvScale.x = 1;
|
|
114
|
+
texture.uvScale.y = 1;
|
|
115
|
+
}
|
|
116
|
+
// Binds (or clears, with null) the texture's image source in place. Does not touch sampling state
|
|
117
|
+
// or the uv-transform.
|
|
118
|
+
export function setTextureImage(texture, image) {
|
|
119
|
+
texture.image = image;
|
|
120
|
+
}
|
|
121
|
+
// Sets the uv offset (scroll/translation) in place. Equivalent to assigning texture.uvOffset
|
|
122
|
+
// directly but provides a named mutator for the KHR_texture_transform model.
|
|
123
|
+
export function setTextureUvOffset(texture, x, y) {
|
|
124
|
+
texture.uvOffset.x = x;
|
|
125
|
+
texture.uvOffset.y = y;
|
|
126
|
+
}
|
|
127
|
+
// Sets the uv rotation in radians in place.
|
|
128
|
+
export function setTextureUvRotation(texture, radians) {
|
|
129
|
+
texture.uvRotation = radians;
|
|
130
|
+
}
|
|
131
|
+
// Sets the uv scale (tiling) in place.
|
|
132
|
+
export function setTextureUvScale(texture, x, y) {
|
|
133
|
+
texture.uvScale.x = x;
|
|
134
|
+
texture.uvScale.y = y;
|
|
135
|
+
}
|
|
136
|
+
// Applies the texture's KHR_texture_transform (scale → rotate → translate) to a single (u, v)
|
|
137
|
+
// coordinate, writing the transformed coordinate into out. Equivalent to multiplying [u, v, 1] by
|
|
138
|
+
// getTextureUvMatrix's result, computed inline to avoid allocating a scratch matrix.
|
|
139
|
+
// Out-param form — out may be any Vector2; no aliasing hazard (u and v are scalar inputs).
|
|
140
|
+
export function transformTextureUv(out, texture, u, v) {
|
|
141
|
+
const r = texture.uvRotation;
|
|
142
|
+
const sx = texture.uvScale.x;
|
|
143
|
+
const sy = texture.uvScale.y;
|
|
144
|
+
const tx = texture.uvOffset.x;
|
|
145
|
+
const ty = texture.uvOffset.y;
|
|
146
|
+
const cosR = Math.cos(r);
|
|
147
|
+
const sinR = Math.sin(r);
|
|
148
|
+
out.x = sx * cosR * u - sy * sinR * v + tx;
|
|
149
|
+
out.y = sx * sinR * u + sy * cosR * v + ty;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=texture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"texture.js","sourceRoot":"","sources":["../src/texture.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/texture",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/entity": "0.1.0",
|
|
31
|
+
"@flighthq/geometry": "0.1.0",
|
|
32
|
+
"@flighthq/types": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.3.0"
|
|
36
|
+
},
|
|
37
|
+
"description": "Textures and samplers: image-backed material texture bindings and cubemaps",
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { ImageResource } from '@flighthq/types';
|
|
2
|
+
import { CubeFaceNegativeX, CubeFacePositiveX, CubeFacePositiveY } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
cloneCubeTexture,
|
|
6
|
+
copyCubeTexture,
|
|
7
|
+
createCubeTexture,
|
|
8
|
+
equalsCubeTexture,
|
|
9
|
+
getCubeTextureFaceSize,
|
|
10
|
+
isCubeTextureComplete,
|
|
11
|
+
setCubeTextureFace,
|
|
12
|
+
} from './cubeTexture';
|
|
13
|
+
import { createSampler, equalsSampler } from './sampler';
|
|
14
|
+
|
|
15
|
+
const fakeFace = { width: 64, height: 64 } as ImageResource;
|
|
16
|
+
const fakeFace2 = { width: 128, height: 128 } as ImageResource;
|
|
17
|
+
|
|
18
|
+
const allFaces: readonly (ImageResource | null)[] = [fakeFace, fakeFace, fakeFace, fakeFace, fakeFace, fakeFace];
|
|
19
|
+
|
|
20
|
+
describe('cloneCubeTexture', () => {
|
|
21
|
+
it('shares the face references in a fresh array and deep-clones the sampler', () => {
|
|
22
|
+
const source = createCubeTexture({
|
|
23
|
+
colorSpace: 'linear',
|
|
24
|
+
faces: [fakeFace, null, null, null, null, null],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const copy = cloneCubeTexture(source);
|
|
28
|
+
|
|
29
|
+
expect(copy).not.toBe(source);
|
|
30
|
+
expect(copy.colorSpace).toStrictEqual('linear');
|
|
31
|
+
expect(copy.faces).not.toBe(source.faces);
|
|
32
|
+
expect(copy.faces[0]).toBe(fakeFace);
|
|
33
|
+
expect(copy.sampler).not.toBe(source.sampler);
|
|
34
|
+
expect(equalsSampler(copy.sampler, source.sampler)).toBe(true);
|
|
35
|
+
|
|
36
|
+
// A fresh faces array means reassigning a face on the clone cannot reach the source.
|
|
37
|
+
expect(copy.faces).not.toBe(source.faces);
|
|
38
|
+
expect(source.faces[1]).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('copyCubeTexture', () => {
|
|
43
|
+
it('writes every field from source into a distinct out, preserving sampler identity', () => {
|
|
44
|
+
const source = createCubeTexture({
|
|
45
|
+
colorSpace: 'linear',
|
|
46
|
+
faces: [fakeFace, null, null, null, null, null],
|
|
47
|
+
});
|
|
48
|
+
const out = createCubeTexture();
|
|
49
|
+
const outSampler = out.sampler;
|
|
50
|
+
|
|
51
|
+
copyCubeTexture(out, source);
|
|
52
|
+
|
|
53
|
+
expect(out.colorSpace).toStrictEqual('linear');
|
|
54
|
+
expect(out.faces[0]).toBe(fakeFace);
|
|
55
|
+
expect(out.sampler).toBe(outSampler);
|
|
56
|
+
expect(equalsSampler(out.sampler, source.sampler)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('is safe when out aliases source', () => {
|
|
60
|
+
const cube = createCubeTexture({
|
|
61
|
+
colorSpace: 'linear',
|
|
62
|
+
faces: [fakeFace, null, null, null, null, null],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
copyCubeTexture(cube, cube);
|
|
66
|
+
|
|
67
|
+
expect(cube.colorSpace).toStrictEqual('linear');
|
|
68
|
+
expect(cube.faces[0]).toBe(fakeFace);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('createCubeTexture', () => {
|
|
73
|
+
it('applies the default six unbound faces, srgb, default sampler', () => {
|
|
74
|
+
const cube = createCubeTexture();
|
|
75
|
+
|
|
76
|
+
expect(cube.faces).toHaveLength(6);
|
|
77
|
+
expect(cube.faces.every((face) => face === null)).toBe(true);
|
|
78
|
+
expect(cube.colorSpace).toStrictEqual('srgb');
|
|
79
|
+
expect(equalsSampler(cube.sampler, createSampler())).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('copies a supplied faces array rather than aliasing it', () => {
|
|
83
|
+
const faces = [fakeFace, null, null, null, null, null];
|
|
84
|
+
const cube = createCubeTexture({ faces });
|
|
85
|
+
|
|
86
|
+
expect(cube.faces).not.toBe(faces);
|
|
87
|
+
expect(cube.faces[0]).toBe(fakeFace);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('equalsCubeTexture', () => {
|
|
92
|
+
it('is true for cubes with the same color space, sampler, and face references', () => {
|
|
93
|
+
const a = createCubeTexture({ colorSpace: 'linear', faces: allFaces });
|
|
94
|
+
const b = createCubeTexture({ colorSpace: 'linear', faces: allFaces });
|
|
95
|
+
|
|
96
|
+
expect(equalsCubeTexture(a, b)).toBe(true);
|
|
97
|
+
expect(equalsCubeTexture(a, a)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('is false when colorSpace differs', () => {
|
|
101
|
+
const a = createCubeTexture({ colorSpace: 'linear' });
|
|
102
|
+
const b = createCubeTexture({ colorSpace: 'srgb' });
|
|
103
|
+
|
|
104
|
+
expect(equalsCubeTexture(a, b)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('is false when a face reference differs', () => {
|
|
108
|
+
const a = createCubeTexture({ faces: allFaces });
|
|
109
|
+
const b = createCubeTexture({ faces: allFaces });
|
|
110
|
+
(b.faces as (ImageResource | null)[])[0] = fakeFace2;
|
|
111
|
+
|
|
112
|
+
expect(equalsCubeTexture(a, b)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('is false when the sampler differs', () => {
|
|
116
|
+
const a = createCubeTexture();
|
|
117
|
+
const b = createCubeTexture({ sampler: createSampler({ mipmaps: false }) });
|
|
118
|
+
|
|
119
|
+
expect(equalsCubeTexture(a, b)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('is false for null or undefined operands', () => {
|
|
123
|
+
const a = createCubeTexture();
|
|
124
|
+
|
|
125
|
+
expect(equalsCubeTexture(a, null)).toBe(false);
|
|
126
|
+
expect(equalsCubeTexture(null, a)).toBe(false);
|
|
127
|
+
expect(equalsCubeTexture(undefined, undefined)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('getCubeTextureFaceSize', () => {
|
|
132
|
+
it('returns the width of the first non-null face', () => {
|
|
133
|
+
const cube = createCubeTexture({ faces: [null, fakeFace, null, null, null, null] });
|
|
134
|
+
|
|
135
|
+
expect(getCubeTextureFaceSize(cube)).toStrictEqual(64);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns -1 when all faces are null', () => {
|
|
139
|
+
const cube = createCubeTexture();
|
|
140
|
+
|
|
141
|
+
expect(getCubeTextureFaceSize(cube)).toStrictEqual(-1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('isCubeTextureComplete', () => {
|
|
146
|
+
it('is false when any face is null', () => {
|
|
147
|
+
const cube = createCubeTexture({ faces: [fakeFace, null, null, null, null, null] });
|
|
148
|
+
|
|
149
|
+
expect(isCubeTextureComplete(cube)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('is true when all six faces are bound', () => {
|
|
153
|
+
const cube = createCubeTexture({ faces: allFaces });
|
|
154
|
+
|
|
155
|
+
expect(isCubeTextureComplete(cube)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('setCubeTextureFace', () => {
|
|
160
|
+
it('binds a face using a named face constant', () => {
|
|
161
|
+
const cube = createCubeTexture();
|
|
162
|
+
|
|
163
|
+
setCubeTextureFace(cube, CubeFacePositiveX, fakeFace);
|
|
164
|
+
expect(cube.faces[CubeFacePositiveX]).toBe(fakeFace);
|
|
165
|
+
expect(cube.faces[CubeFaceNegativeX]).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('unbinds a face when passed null', () => {
|
|
169
|
+
const cube = createCubeTexture({ faces: allFaces });
|
|
170
|
+
|
|
171
|
+
setCubeTextureFace(cube, CubeFacePositiveY, null);
|
|
172
|
+
expect(cube.faces[CubeFacePositiveY]).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneSampler,
|
|
3
|
+
copySampler,
|
|
4
|
+
createAnisotropicSampler,
|
|
5
|
+
createClampLinearSampler,
|
|
6
|
+
createPixelArtSampler,
|
|
7
|
+
createSampler,
|
|
8
|
+
createTilingSampler,
|
|
9
|
+
equalsSampler,
|
|
10
|
+
} from './sampler';
|
|
11
|
+
|
|
12
|
+
describe('cloneSampler', () => {
|
|
13
|
+
it('copies every field into an independent entity', () => {
|
|
14
|
+
const source = createSampler({
|
|
15
|
+
anisotropy: 8,
|
|
16
|
+
magFilter: 'nearest',
|
|
17
|
+
minFilter: 'nearest-mipmap-nearest',
|
|
18
|
+
mipmaps: false,
|
|
19
|
+
wrapU: 'repeat',
|
|
20
|
+
wrapV: 'mirror-repeat',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const copy = cloneSampler(source);
|
|
24
|
+
|
|
25
|
+
expect(copy).not.toBe(source);
|
|
26
|
+
expect(equalsSampler(copy, source)).toBe(true);
|
|
27
|
+
|
|
28
|
+
copy.anisotropy = 2;
|
|
29
|
+
expect(source.anisotropy).toStrictEqual(8);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('copySampler', () => {
|
|
34
|
+
it('writes every field from source into a distinct out', () => {
|
|
35
|
+
const source = createSampler({ anisotropy: 4, mipmaps: false, wrapU: 'repeat' });
|
|
36
|
+
const out = createSampler();
|
|
37
|
+
|
|
38
|
+
copySampler(out, source);
|
|
39
|
+
|
|
40
|
+
expect(out).not.toBe(source);
|
|
41
|
+
expect(equalsSampler(out, source)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is safe when out aliases source', () => {
|
|
45
|
+
const source = createSampler({ anisotropy: 4, wrapV: 'mirror-repeat' });
|
|
46
|
+
|
|
47
|
+
copySampler(source, source);
|
|
48
|
+
|
|
49
|
+
expect(source.anisotropy).toStrictEqual(4);
|
|
50
|
+
expect(source.wrapV).toStrictEqual('mirror-repeat');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('createAnisotropicSampler', () => {
|
|
55
|
+
it('sets anisotropy to the supplied level, keeps other defaults', () => {
|
|
56
|
+
const sampler = createAnisotropicSampler(16);
|
|
57
|
+
|
|
58
|
+
expect(sampler.anisotropy).toStrictEqual(16);
|
|
59
|
+
expect(sampler.magFilter).toStrictEqual('linear');
|
|
60
|
+
expect(sampler.minFilter).toStrictEqual('linear-mipmap-linear');
|
|
61
|
+
expect(sampler.mipmaps).toBe(true);
|
|
62
|
+
expect(sampler.wrapU).toStrictEqual('clamp-to-edge');
|
|
63
|
+
expect(sampler.wrapV).toStrictEqual('clamp-to-edge');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('createClampLinearSampler', () => {
|
|
68
|
+
it('produces the default sampler state', () => {
|
|
69
|
+
const sampler = createClampLinearSampler();
|
|
70
|
+
|
|
71
|
+
expect(equalsSampler(sampler, createSampler())).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('createPixelArtSampler', () => {
|
|
76
|
+
it('uses nearest-neighbor filtering, clamp-to-edge, and no mipmaps', () => {
|
|
77
|
+
const sampler = createPixelArtSampler();
|
|
78
|
+
|
|
79
|
+
expect(sampler.magFilter).toStrictEqual('nearest');
|
|
80
|
+
expect(sampler.minFilter).toStrictEqual('nearest');
|
|
81
|
+
expect(sampler.mipmaps).toBe(false);
|
|
82
|
+
expect(sampler.wrapU).toStrictEqual('clamp-to-edge');
|
|
83
|
+
expect(sampler.wrapV).toStrictEqual('clamp-to-edge');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('createSampler', () => {
|
|
88
|
+
it('applies the default sampling state', () => {
|
|
89
|
+
const sampler = createSampler();
|
|
90
|
+
|
|
91
|
+
expect(sampler.anisotropy).toStrictEqual(1);
|
|
92
|
+
expect(sampler.magFilter).toStrictEqual('linear');
|
|
93
|
+
expect(sampler.minFilter).toStrictEqual('linear-mipmap-linear');
|
|
94
|
+
expect(sampler.mipmaps).toBe(true);
|
|
95
|
+
expect(sampler.wrapU).toStrictEqual('clamp-to-edge');
|
|
96
|
+
expect(sampler.wrapV).toStrictEqual('clamp-to-edge');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('overrides only the supplied fields', () => {
|
|
100
|
+
const sampler = createSampler({ anisotropy: 16, wrapU: 'repeat' });
|
|
101
|
+
|
|
102
|
+
expect(sampler.anisotropy).toStrictEqual(16);
|
|
103
|
+
expect(sampler.wrapU).toStrictEqual('repeat');
|
|
104
|
+
expect(sampler.wrapV).toStrictEqual('clamp-to-edge');
|
|
105
|
+
expect(sampler.mipmaps).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('createTilingSampler', () => {
|
|
110
|
+
it('uses repeat wrap on both axes with trilinear filtering', () => {
|
|
111
|
+
const sampler = createTilingSampler();
|
|
112
|
+
|
|
113
|
+
expect(sampler.wrapU).toStrictEqual('repeat');
|
|
114
|
+
expect(sampler.wrapV).toStrictEqual('repeat');
|
|
115
|
+
expect(sampler.magFilter).toStrictEqual('linear');
|
|
116
|
+
expect(sampler.minFilter).toStrictEqual('linear-mipmap-linear');
|
|
117
|
+
expect(sampler.mipmaps).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('equalsSampler', () => {
|
|
122
|
+
it('is true for identical state and the same reference', () => {
|
|
123
|
+
const a = createSampler({ anisotropy: 4 });
|
|
124
|
+
const b = createSampler({ anisotropy: 4 });
|
|
125
|
+
|
|
126
|
+
expect(equalsSampler(a, b)).toBe(true);
|
|
127
|
+
expect(equalsSampler(a, a)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is false when any field differs', () => {
|
|
131
|
+
const a = createSampler();
|
|
132
|
+
const b = createSampler({ mipmaps: false });
|
|
133
|
+
|
|
134
|
+
expect(equalsSampler(a, b)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('is false for null or undefined operands', () => {
|
|
138
|
+
const a = createSampler();
|
|
139
|
+
|
|
140
|
+
expect(equalsSampler(a, null)).toBe(false);
|
|
141
|
+
expect(equalsSampler(null, a)).toBe(false);
|
|
142
|
+
expect(equalsSampler(undefined, undefined)).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { createMatrix3, createVector2, inverseMatrix3 } from '@flighthq/geometry';
|
|
2
|
+
import type { ImageResource } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { createSampler, equalsSampler } from './sampler';
|
|
5
|
+
import {
|
|
6
|
+
cloneTexture,
|
|
7
|
+
copyTexture,
|
|
8
|
+
createTexture,
|
|
9
|
+
equalsTexture,
|
|
10
|
+
getTextureHeight,
|
|
11
|
+
getTextureInverseUvMatrix,
|
|
12
|
+
getTextureUvMatrix,
|
|
13
|
+
getTextureWidth,
|
|
14
|
+
isTextureReady,
|
|
15
|
+
resetTextureUvTransform,
|
|
16
|
+
setTextureImage,
|
|
17
|
+
setTextureUvOffset,
|
|
18
|
+
setTextureUvRotation,
|
|
19
|
+
setTextureUvScale,
|
|
20
|
+
transformTextureUv,
|
|
21
|
+
} from './texture';
|
|
22
|
+
|
|
23
|
+
const fakeImage = { width: 32, height: 64 } as ImageResource;
|
|
24
|
+
|
|
25
|
+
describe('cloneTexture', () => {
|
|
26
|
+
it('shares the image but deep-clones the sampler and uv vectors', () => {
|
|
27
|
+
const source = createTexture({
|
|
28
|
+
colorSpace: 'linear',
|
|
29
|
+
image: fakeImage,
|
|
30
|
+
uvRotation: 0.5,
|
|
31
|
+
});
|
|
32
|
+
source.uvOffset.x = 0.25;
|
|
33
|
+
source.uvScale.y = 3;
|
|
34
|
+
|
|
35
|
+
const copy = cloneTexture(source);
|
|
36
|
+
|
|
37
|
+
expect(copy).not.toBe(source);
|
|
38
|
+
expect(copy.image).toBe(fakeImage);
|
|
39
|
+
expect(copy.colorSpace).toStrictEqual('linear');
|
|
40
|
+
expect(copy.uvRotation).toStrictEqual(0.5);
|
|
41
|
+
expect(copy.sampler).not.toBe(source.sampler);
|
|
42
|
+
expect(equalsSampler(copy.sampler, source.sampler)).toBe(true);
|
|
43
|
+
expect(copy.uvOffset).not.toBe(source.uvOffset);
|
|
44
|
+
expect(copy.uvOffset.x).toStrictEqual(0.25);
|
|
45
|
+
expect(copy.uvScale.y).toStrictEqual(3);
|
|
46
|
+
|
|
47
|
+
copy.uvOffset.x = 0.9;
|
|
48
|
+
expect(source.uvOffset.x).toStrictEqual(0.25);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('copyTexture', () => {
|
|
53
|
+
it('writes every field from source into a distinct out, preserving out entity identities', () => {
|
|
54
|
+
const source = createTexture({ colorSpace: 'linear', image: fakeImage, uvRotation: 1 });
|
|
55
|
+
source.uvScale.x = 4;
|
|
56
|
+
const out = createTexture();
|
|
57
|
+
const outSampler = out.sampler;
|
|
58
|
+
const outOffset = out.uvOffset;
|
|
59
|
+
|
|
60
|
+
copyTexture(out, source);
|
|
61
|
+
|
|
62
|
+
expect(out.image).toBe(fakeImage);
|
|
63
|
+
expect(out.colorSpace).toStrictEqual('linear');
|
|
64
|
+
expect(out.uvRotation).toStrictEqual(1);
|
|
65
|
+
expect(out.uvScale.x).toStrictEqual(4);
|
|
66
|
+
expect(out.sampler).toBe(outSampler);
|
|
67
|
+
expect(out.uvOffset).toBe(outOffset);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('is safe when out aliases source', () => {
|
|
71
|
+
const source = createTexture({ colorSpace: 'linear', image: fakeImage, uvRotation: 2 });
|
|
72
|
+
source.uvScale.x = 7;
|
|
73
|
+
|
|
74
|
+
copyTexture(source, source);
|
|
75
|
+
|
|
76
|
+
expect(source.colorSpace).toStrictEqual('linear');
|
|
77
|
+
expect(source.image).toBe(fakeImage);
|
|
78
|
+
expect(source.uvRotation).toStrictEqual(2);
|
|
79
|
+
expect(source.uvScale.x).toStrictEqual(7);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('createTexture', () => {
|
|
84
|
+
it('applies the default unbound, srgb, identity-transform state', () => {
|
|
85
|
+
const texture = createTexture();
|
|
86
|
+
|
|
87
|
+
expect(texture.image).toBeNull();
|
|
88
|
+
expect(texture.colorSpace).toStrictEqual('srgb');
|
|
89
|
+
expect(texture.uvRotation).toStrictEqual(0);
|
|
90
|
+
expect(texture.uvOffset.x).toStrictEqual(0);
|
|
91
|
+
expect(texture.uvOffset.y).toStrictEqual(0);
|
|
92
|
+
expect(texture.uvScale.x).toStrictEqual(1);
|
|
93
|
+
expect(texture.uvScale.y).toStrictEqual(1);
|
|
94
|
+
expect(equalsSampler(texture.sampler, createSampler())).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('clones supplied sampler and uv vectors rather than aliasing them', () => {
|
|
98
|
+
const sampler = createSampler({ anisotropy: 8 });
|
|
99
|
+
const texture = createTexture({ sampler });
|
|
100
|
+
|
|
101
|
+
expect(texture.sampler).not.toBe(sampler);
|
|
102
|
+
expect(texture.sampler.anisotropy).toStrictEqual(8);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('equalsTexture', () => {
|
|
107
|
+
it('is true for textures with identical state and same image reference', () => {
|
|
108
|
+
const a = createTexture({ image: fakeImage, colorSpace: 'linear' });
|
|
109
|
+
const b = createTexture({ image: fakeImage, colorSpace: 'linear' });
|
|
110
|
+
|
|
111
|
+
expect(equalsTexture(a, b)).toBe(true);
|
|
112
|
+
expect(equalsTexture(a, a)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('is false when the image reference differs', () => {
|
|
116
|
+
const other = { width: 4, height: 4 } as ImageResource;
|
|
117
|
+
const a = createTexture({ image: fakeImage });
|
|
118
|
+
const b = createTexture({ image: other });
|
|
119
|
+
|
|
120
|
+
expect(equalsTexture(a, b)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('is false when colorSpace differs', () => {
|
|
124
|
+
const a = createTexture({ colorSpace: 'linear' });
|
|
125
|
+
const b = createTexture({ colorSpace: 'srgb' });
|
|
126
|
+
|
|
127
|
+
expect(equalsTexture(a, b)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is false when uvRotation differs', () => {
|
|
131
|
+
const a = createTexture({ uvRotation: 0.5 });
|
|
132
|
+
const b = createTexture({ uvRotation: 0 });
|
|
133
|
+
|
|
134
|
+
expect(equalsTexture(a, b)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('is false when uvOffset differs', () => {
|
|
138
|
+
const a = createTexture();
|
|
139
|
+
const b = createTexture();
|
|
140
|
+
b.uvOffset.x = 0.5;
|
|
141
|
+
|
|
142
|
+
expect(equalsTexture(a, b)).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('is false when uvScale differs', () => {
|
|
146
|
+
const a = createTexture();
|
|
147
|
+
const b = createTexture();
|
|
148
|
+
b.uvScale.y = 2;
|
|
149
|
+
|
|
150
|
+
expect(equalsTexture(a, b)).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('is false when the sampler differs', () => {
|
|
154
|
+
const a = createTexture();
|
|
155
|
+
const b = createTexture({ sampler: createSampler({ mipmaps: false }) });
|
|
156
|
+
|
|
157
|
+
expect(equalsTexture(a, b)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('is false for null or undefined operands', () => {
|
|
161
|
+
const a = createTexture();
|
|
162
|
+
|
|
163
|
+
expect(equalsTexture(a, null)).toBe(false);
|
|
164
|
+
expect(equalsTexture(null, a)).toBe(false);
|
|
165
|
+
expect(equalsTexture(undefined, undefined)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getTextureHeight', () => {
|
|
170
|
+
it('returns the image height when an image is bound', () => {
|
|
171
|
+
const texture = createTexture({ image: fakeImage });
|
|
172
|
+
|
|
173
|
+
expect(getTextureHeight(texture)).toStrictEqual(64);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns -1 when no image is bound', () => {
|
|
177
|
+
const texture = createTexture();
|
|
178
|
+
|
|
179
|
+
expect(getTextureHeight(texture)).toStrictEqual(-1);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('getTextureInverseUvMatrix', () => {
|
|
184
|
+
it('produces the identity matrix for the default uv transform', () => {
|
|
185
|
+
const texture = createTexture();
|
|
186
|
+
const out = createMatrix3();
|
|
187
|
+
|
|
188
|
+
getTextureInverseUvMatrix(out, texture);
|
|
189
|
+
|
|
190
|
+
expect(out.m[0]).toBeCloseTo(1);
|
|
191
|
+
expect(out.m[1]).toBeCloseTo(0);
|
|
192
|
+
expect(out.m[2]).toBeCloseTo(0);
|
|
193
|
+
expect(out.m[3]).toBeCloseTo(0);
|
|
194
|
+
expect(out.m[4]).toBeCloseTo(1);
|
|
195
|
+
expect(out.m[5]).toBeCloseTo(0);
|
|
196
|
+
expect(out.m[8]).toBeCloseTo(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('equals inverting the composed forward uv matrix', () => {
|
|
200
|
+
const texture = createTexture({ uvRotation: Math.PI / 6 });
|
|
201
|
+
setTextureUvScale(texture, 2, 3);
|
|
202
|
+
setTextureUvOffset(texture, 0.1, 0.2);
|
|
203
|
+
|
|
204
|
+
// The documented contract: compose getTextureUvMatrix, then inverse it via geometry.
|
|
205
|
+
const expected = createMatrix3();
|
|
206
|
+
getTextureUvMatrix(expected, texture);
|
|
207
|
+
inverseMatrix3(expected, expected);
|
|
208
|
+
|
|
209
|
+
const out = createMatrix3();
|
|
210
|
+
getTextureInverseUvMatrix(out, texture);
|
|
211
|
+
|
|
212
|
+
for (let k = 0; k < 9; k++) {
|
|
213
|
+
expect(out.m[k]).toBeCloseTo(expected.m[k]);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('getTextureUvMatrix', () => {
|
|
219
|
+
it('produces the identity matrix for the default uv transform', () => {
|
|
220
|
+
const texture = createTexture();
|
|
221
|
+
const out = createMatrix3();
|
|
222
|
+
|
|
223
|
+
getTextureUvMatrix(out, texture);
|
|
224
|
+
|
|
225
|
+
// Row-major: identity = [1,0,0; 0,1,0; 0,0,1]
|
|
226
|
+
expect(out.m[0]).toBeCloseTo(1);
|
|
227
|
+
expect(out.m[1]).toBeCloseTo(0);
|
|
228
|
+
expect(out.m[2]).toBeCloseTo(0);
|
|
229
|
+
expect(out.m[3]).toBeCloseTo(0);
|
|
230
|
+
expect(out.m[4]).toBeCloseTo(1);
|
|
231
|
+
expect(out.m[5]).toBeCloseTo(0);
|
|
232
|
+
expect(out.m[6]).toBeCloseTo(0);
|
|
233
|
+
expect(out.m[7]).toBeCloseTo(0);
|
|
234
|
+
expect(out.m[8]).toBeCloseTo(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('encodes offset in the translation column', () => {
|
|
238
|
+
const texture = createTexture();
|
|
239
|
+
setTextureUvOffset(texture, 0.25, 0.75);
|
|
240
|
+
const out = createMatrix3();
|
|
241
|
+
|
|
242
|
+
getTextureUvMatrix(out, texture);
|
|
243
|
+
|
|
244
|
+
expect(out.m[2]).toBeCloseTo(0.25); // tx
|
|
245
|
+
expect(out.m[5]).toBeCloseTo(0.75); // ty
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('encodes scale in the diagonal', () => {
|
|
249
|
+
const texture = createTexture();
|
|
250
|
+
setTextureUvScale(texture, 2, 3);
|
|
251
|
+
const out = createMatrix3();
|
|
252
|
+
|
|
253
|
+
getTextureUvMatrix(out, texture);
|
|
254
|
+
|
|
255
|
+
expect(out.m[0]).toBeCloseTo(2); // sx*cos(0) = sx
|
|
256
|
+
expect(out.m[4]).toBeCloseTo(3); // sy*cos(0) = sy
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('matches KHR_texture_transform formula for a rotated, scaled, offset texture', () => {
|
|
260
|
+
const r = Math.PI / 4;
|
|
261
|
+
const texture = createTexture({ uvRotation: r });
|
|
262
|
+
setTextureUvScale(texture, 2, 2);
|
|
263
|
+
setTextureUvOffset(texture, 0.1, 0.2);
|
|
264
|
+
const out = createMatrix3();
|
|
265
|
+
|
|
266
|
+
getTextureUvMatrix(out, texture);
|
|
267
|
+
|
|
268
|
+
const cosR = Math.cos(r);
|
|
269
|
+
const sinR = Math.sin(r);
|
|
270
|
+
expect(out.m[0]).toBeCloseTo(2 * cosR); // sx*cos(r)
|
|
271
|
+
expect(out.m[1]).toBeCloseTo(-2 * sinR); // -sy*sin(r)
|
|
272
|
+
expect(out.m[2]).toBeCloseTo(0.1); // tx
|
|
273
|
+
expect(out.m[3]).toBeCloseTo(2 * sinR); // sx*sin(r)
|
|
274
|
+
expect(out.m[4]).toBeCloseTo(2 * cosR); // sy*cos(r)
|
|
275
|
+
expect(out.m[5]).toBeCloseTo(0.2); // ty
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('getTextureWidth', () => {
|
|
280
|
+
it('returns the image width when an image is bound', () => {
|
|
281
|
+
const texture = createTexture({ image: fakeImage });
|
|
282
|
+
|
|
283
|
+
expect(getTextureWidth(texture)).toStrictEqual(32);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns -1 when no image is bound', () => {
|
|
287
|
+
const texture = createTexture();
|
|
288
|
+
|
|
289
|
+
expect(getTextureWidth(texture)).toStrictEqual(-1);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('isTextureReady', () => {
|
|
294
|
+
it('is false with a null image and true once bound', () => {
|
|
295
|
+
const texture = createTexture();
|
|
296
|
+
|
|
297
|
+
expect(isTextureReady(texture)).toBe(false);
|
|
298
|
+
|
|
299
|
+
texture.image = fakeImage;
|
|
300
|
+
expect(isTextureReady(texture)).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('resetTextureUvTransform', () => {
|
|
305
|
+
it('restores the identity transform while leaving image, color space, and sampler untouched', () => {
|
|
306
|
+
const texture = createTexture({ colorSpace: 'linear', image: fakeImage });
|
|
307
|
+
const sampler = texture.sampler;
|
|
308
|
+
setTextureUvOffset(texture, 0.4, 0.6);
|
|
309
|
+
setTextureUvRotation(texture, Math.PI);
|
|
310
|
+
setTextureUvScale(texture, 5, 7);
|
|
311
|
+
|
|
312
|
+
resetTextureUvTransform(texture);
|
|
313
|
+
|
|
314
|
+
expect(texture.uvOffset.x).toStrictEqual(0);
|
|
315
|
+
expect(texture.uvOffset.y).toStrictEqual(0);
|
|
316
|
+
expect(texture.uvRotation).toStrictEqual(0);
|
|
317
|
+
expect(texture.uvScale.x).toStrictEqual(1);
|
|
318
|
+
expect(texture.uvScale.y).toStrictEqual(1);
|
|
319
|
+
expect(texture.colorSpace).toStrictEqual('linear');
|
|
320
|
+
expect(texture.image).toBe(fakeImage);
|
|
321
|
+
expect(texture.sampler).toBe(sampler);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('setTextureImage', () => {
|
|
326
|
+
it('binds and clears the image in place', () => {
|
|
327
|
+
const texture = createTexture();
|
|
328
|
+
|
|
329
|
+
setTextureImage(texture, fakeImage);
|
|
330
|
+
expect(texture.image).toBe(fakeImage);
|
|
331
|
+
|
|
332
|
+
setTextureImage(texture, null);
|
|
333
|
+
expect(texture.image).toBeNull();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('setTextureUvOffset', () => {
|
|
338
|
+
it('updates the uvOffset in place', () => {
|
|
339
|
+
const texture = createTexture();
|
|
340
|
+
|
|
341
|
+
setTextureUvOffset(texture, 0.3, 0.7);
|
|
342
|
+
|
|
343
|
+
expect(texture.uvOffset.x).toBeCloseTo(0.3);
|
|
344
|
+
expect(texture.uvOffset.y).toBeCloseTo(0.7);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('setTextureUvRotation', () => {
|
|
349
|
+
it('updates uvRotation in place', () => {
|
|
350
|
+
const texture = createTexture();
|
|
351
|
+
|
|
352
|
+
setTextureUvRotation(texture, Math.PI);
|
|
353
|
+
|
|
354
|
+
expect(texture.uvRotation).toBeCloseTo(Math.PI);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('setTextureUvScale', () => {
|
|
359
|
+
it('updates the uvScale in place', () => {
|
|
360
|
+
const texture = createTexture();
|
|
361
|
+
|
|
362
|
+
setTextureUvScale(texture, 4, 8);
|
|
363
|
+
|
|
364
|
+
expect(texture.uvScale.x).toBeCloseTo(4);
|
|
365
|
+
expect(texture.uvScale.y).toBeCloseTo(8);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('transformTextureUv', () => {
|
|
370
|
+
it('leaves a coordinate unchanged under the identity transform', () => {
|
|
371
|
+
const texture = createTexture();
|
|
372
|
+
const out = createVector2(0, 0);
|
|
373
|
+
|
|
374
|
+
transformTextureUv(out, texture, 0.25, 0.75);
|
|
375
|
+
|
|
376
|
+
expect(out.x).toBeCloseTo(0.25);
|
|
377
|
+
expect(out.y).toBeCloseTo(0.75);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('applies scale then offset', () => {
|
|
381
|
+
const texture = createTexture();
|
|
382
|
+
setTextureUvScale(texture, 2, 3);
|
|
383
|
+
setTextureUvOffset(texture, 0.1, 0.2);
|
|
384
|
+
const out = createVector2(0, 0);
|
|
385
|
+
|
|
386
|
+
transformTextureUv(out, texture, 0.5, 0.5);
|
|
387
|
+
|
|
388
|
+
expect(out.x).toBeCloseTo(2 * 0.5 + 0.1);
|
|
389
|
+
expect(out.y).toBeCloseTo(3 * 0.5 + 0.2);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('matches multiplying the coordinate by getTextureUvMatrix', () => {
|
|
393
|
+
const texture = createTexture({ uvRotation: Math.PI / 3 });
|
|
394
|
+
setTextureUvScale(texture, 1.5, 2.5);
|
|
395
|
+
setTextureUvOffset(texture, 0.2, 0.4);
|
|
396
|
+
const matrix = createMatrix3();
|
|
397
|
+
getTextureUvMatrix(matrix, texture);
|
|
398
|
+
const u = 0.3;
|
|
399
|
+
const v = 0.8;
|
|
400
|
+
const m = matrix.m;
|
|
401
|
+
const expectedX = m[0] * u + m[1] * v + m[2];
|
|
402
|
+
const expectedY = m[3] * u + m[4] * v + m[5];
|
|
403
|
+
const out = createVector2(0, 0);
|
|
404
|
+
|
|
405
|
+
transformTextureUv(out, texture, u, v);
|
|
406
|
+
|
|
407
|
+
expect(out.x).toBeCloseTo(expectedX);
|
|
408
|
+
expect(out.y).toBeCloseTo(expectedY);
|
|
409
|
+
});
|
|
410
|
+
});
|