diff --git a/v2/packages/engine/src/render/__tests__/particleLights.test.ts b/v2/packages/engine/src/render/__tests__/particleLights.test.ts new file mode 100644 index 0000000..184364e --- /dev/null +++ b/v2/packages/engine/src/render/__tests__/particleLights.test.ts @@ -0,0 +1,79 @@ +/** + * Тесты света частиц: блок юниформ (дефолты), упаковка снапшота источников + * (цвет × яркость, кап), фонарик (нормировка направления, конус), сброс. + */ +import { describe, expect, it } from 'vitest'; +import { makeParticleLightUniforms, packParticleLights, PARTICLE_LIGHT_MAX } from '../particles'; +import type { ParticleLight, ParticleSpot } from '../particles'; + +describe('свет частиц (packParticleLights)', () => { + it('блок юниформ: по умолчанию источников нет, фонарик погашен', () => { + const u = makeParticleLightUniforms(); + expect(u.uLightCount!.value).toBe(0); + expect(u.uSpotOn!.value).toBe(0); + expect((u.uLightPos!.value as Float32Array).length).toBe(PARTICLE_LIGHT_MAX * 3); + }); + + it('упаковка: цвет × яркость, радиус и позиция по местам', () => { + const u = makeParticleLightUniforms(); + const points: ParticleLight[] = [ + { pos: [10, 2, 20], color: 0xff8040, intensity: 0.5, radius: 30 }, + { pos: [0, 2, 0], color: 0x202020, intensity: 2, radius: 5 }, + ]; + packParticleLights(u, points, null); + expect(u.uLightCount!.value).toBe(2); + const col = u.uLightColor!.value as Float32Array; + // 0xff8040 × 0.5 → (0.5, 0.25, 0.125) + expect(col[0]).toBeCloseTo(0.5); + expect(col[1]).toBeCloseTo(0.251, 2); + expect(col[2]).toBeCloseTo(0.125, 2); + // второй источник: 0x20 × 2 → 0.25 + expect(col[5]).toBeCloseTo(0.25); + const pos = u.uLightPos!.value as Float32Array; + expect([pos[0], pos[1], pos[2]]).toEqual([10, 2, 20]); + expect((u.uLightRadius!.value as Float32Array)[1]).toBe(5); + expect(u.uSpotOn!.value).toBe(0); + }); + + it('кап: лишние источники обрезаются до PARTICLE_LIGHT_MAX', () => { + const u = makeParticleLightUniforms(); + const many: ParticleLight[] = Array.from({ length: 40 }, (_, i) => ({ + pos: [i, 0, 0], color: 0xffffff, intensity: 1, radius: 1, + })); + packParticleLights(u, many, null); + expect(u.uLightCount!.value).toBe(PARTICLE_LIGHT_MAX); + }); + + it('фонарик: направление нормируется, конус от угла и penumbra', () => { + const u = makeParticleLightUniforms(); + const spot: ParticleSpot = { + pos: [5, 7, 5], dir: [0, -3, 0], color: 0xfff2c0, intensity: 0.8, + distance: 60, angle: Math.PI / 4, penumbra: 0.4, + }; + packParticleLights(u, [], spot); + expect(u.uSpotOn!.value).toBe(1); + const dir = u.uSpotDir!.value as Float32Array; + expect([dir[0], dir[1], dir[2]]).toEqual([0, -1, 0]); // нормировка + expect(u.uSpotCos!.value).toBeCloseTo(Math.cos(Math.PI / 8)); + // внутренний край — уже угол, косинус больше внешнего + expect(u.uSpotSoft!.value).toBeGreaterThan(u.uSpotCos!.value); + const c = u.uSpotColor!.value as Float32Array; + expect(c[0]).toBeCloseTo(0.8); + expect(c[1]).toBeCloseTo((0xf2 / 255) * 0.8); + }); + + it('сброс: погасший фонарик гасит uSpotOn, счётчик обнуляется', () => { + const u = makeParticleLightUniforms(); + packParticleLights(u, [], { + pos: [0, 0, 0], dir: [1, 0, 0], color: 0xffffff, intensity: 1, + distance: 10, angle: 1, penumbra: 0.5, + }); + expect(u.uSpotOn!.value).toBe(1); + packParticleLights(u, [], null); + expect(u.uSpotOn!.value).toBe(0); + packParticleLights(u, [{ pos: [1, 1, 1], color: 0xffffff, intensity: 1, radius: 2 }], null); + expect(u.uLightCount!.value).toBe(1); + packParticleLights(u, [], null); + expect(u.uLightCount!.value).toBe(0); + }); +}); \ No newline at end of file