136 lines
6.5 KiB
JavaScript
136 lines
6.5 KiB
JavaScript
import * as THREE from 'three'
|
|
import { PackOpening, packSize, rollPackContents } from '../src/packOpening.ts'
|
|
import { createStudioCubeTexture } from '../src/cardMaterial.ts'
|
|
|
|
export async function verifyPackPreparation() {
|
|
const startedAt = performance.now()
|
|
// Fixed pseudo-random contents keep the full current pack reproducible.
|
|
let seed = 12345
|
|
const contents = rollPackContents(['David', 'Timothy'], () => {
|
|
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
|
return seed / 4294967296
|
|
})
|
|
if (contents.length !== packSize) throw new Error('Unexpected pack size')
|
|
const loader = new THREE.TextureLoader()
|
|
const paths = ['david-front', 'david-finish-mask', 'timothy-front', 'timothy-finish-mask', 'card-back', 'linen-normal']
|
|
const textures = await Promise.all(paths.map((path) => loader.loadAsync(`/reference/${path}.png`)))
|
|
for (const index of [0, 2, 4]) textures[index].colorSpace = THREE.SRGBColorSpace
|
|
textures[5].wrapS = textures[5].wrapT = THREE.RepeatWrapping
|
|
const environment = createStudioCubeTexture()
|
|
const results = []
|
|
try {
|
|
for (const lightType of ['Point', 'Directional', 'Spot']) {
|
|
// A fresh context prevents the preceding pixel test from accidentally warming pack shaders.
|
|
const renderer = new THREE.WebGLRenderer({ antialias: false })
|
|
renderer.setSize(192, 264)
|
|
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
const shaderErrors = []
|
|
renderer.debug.onShaderError = (gl, program, vertex, fragment) => {
|
|
shaderErrors.push([gl.getProgramInfoLog(program), gl.getShaderInfoLog(vertex),
|
|
gl.getShaderInfoLog(fragment)].join('\n'))
|
|
}
|
|
const scene = new THREE.Scene()
|
|
scene.background = new THREE.Color('#0b0e14')
|
|
scene.environment = environment
|
|
const lightPosition = new THREE.Vector3(2.2, 2.5, 4.4)
|
|
const light = lightType === 'Point' ? new THREE.PointLight('#fff0d0', 34, 20, 1.7)
|
|
: lightType === 'Directional' ? new THREE.DirectionalLight('#fff0d0', 5.4)
|
|
: new THREE.SpotLight('#fff0d0', 54, 20, THREE.MathUtils.degToRad(28), 0.55, 1.7)
|
|
light.position.copy(lightPosition)
|
|
scene.add(light)
|
|
if (light.target) scene.add(light.target)
|
|
const backMaterial = new THREE.MeshPhysicalMaterial({
|
|
map: textures[4], roughness: 0.42, metalness: 0, clearcoat: 0.2,
|
|
clearcoatRoughness: 0.24, normalMap: null, side: THREE.FrontSide,
|
|
})
|
|
const pack = new PackOpening({
|
|
canvas: renderer.domElement,
|
|
contents,
|
|
textures: new Map([
|
|
['David', { artwork: textures[0], mask: textures[1] }],
|
|
['Timothy', { artwork: textures[2], mask: textures[3] }],
|
|
]),
|
|
backMaterial, normalMap: textures[5], environment, lightPosition, onChange() {},
|
|
})
|
|
pack.resize(192, 264)
|
|
const revealed = []
|
|
renderer.domElement.addEventListener('packreveal', (event) => revealed.push(event.detail))
|
|
const originalNow = performance.now
|
|
try {
|
|
const programsBeforePreparation = renderer.info.programs.length
|
|
await pack.prepareGPU(renderer, scene)
|
|
if (pack.root.visible || pack.state !== 'sealed' || revealed.length) {
|
|
throw new Error('Preparation changed visibility or reveal state')
|
|
}
|
|
const preparedPrograms = new Set(renderer.info.programs.map((program) => program.id))
|
|
if (!preparedPrograms.size) throw new Error('Preparation compiled no programs')
|
|
const texturesAfterPreparation = renderer.info.memory.textures
|
|
const unexpectedPrograms = new Map()
|
|
const visitedStates = new Set()
|
|
let renderedFrames = 0
|
|
let maximumTextureCount = texturesAfterPreparation
|
|
const textureGrowth = []
|
|
let now = originalNow.call(performance)
|
|
performance.now = () => now
|
|
function frame() {
|
|
pack.tick(now)
|
|
renderer.render(scene, pack.camera)
|
|
renderedFrames++
|
|
visitedStates.add(pack.state)
|
|
if (renderer.info.memory.textures > maximumTextureCount) {
|
|
textureGrowth.push({ state: pack.state, count: renderer.info.memory.textures })
|
|
}
|
|
maximumTextureCount = Math.max(maximumTextureCount, renderer.info.memory.textures)
|
|
for (const program of renderer.info.programs) {
|
|
if (!preparedPrograms.has(program.id) && !unexpectedPrograms.has(program.id)) {
|
|
unexpectedPrograms.set(program.id, { id: program.id, name: program.name, firstState: pack.state })
|
|
}
|
|
}
|
|
const gl = renderer.getContext()
|
|
if (shaderErrors.length || gl.isContextLost() || gl.getError() !== gl.NO_ERROR) {
|
|
throw new Error(`Pack WebGL rendering failed: ${shaderErrors.join('\n')}`)
|
|
}
|
|
}
|
|
scene.add(pack.root)
|
|
pack.setActive(true)
|
|
frame()
|
|
const durations = { opening: 3200, lifting: 620, revealing: 820, advancing: 650 }
|
|
for (let action = 0; action < 1 + contents.length * 3 && pack.state !== 'complete'; action++) {
|
|
pack.primary()
|
|
const duration = durations[pack.state]
|
|
if (!duration) throw new Error(`Unexpected animated state: ${pack.state}`)
|
|
const started = now
|
|
for (const progress of [0, 0.1, 0.25, 0.4, 0.5, 0.65, 0.8, 0.9, 1.001]) {
|
|
now = started + duration * progress
|
|
frame()
|
|
}
|
|
}
|
|
if (pack.state !== 'complete' || JSON.stringify(revealed.map(({ fixture, finish, substrate }) =>
|
|
({ fixture, finish, substrate }))) !== JSON.stringify(contents)) {
|
|
throw new Error('First reveal cycle did not complete in selected order')
|
|
}
|
|
pack.restart()
|
|
frame()
|
|
results.push({
|
|
lightType, passed: unexpectedPrograms.size === 0, programsBeforePreparation,
|
|
programsAfterPreparation: preparedPrograms.size,
|
|
programsAfterRevealAndRestart: renderer.info.programs.length,
|
|
unexpectedPrograms: [...unexpectedPrograms.values()], texturesAfterPreparation,
|
|
maximumTextureCount, textureGrowth, renderedFrames, visitedStates: [...visitedStates], revealed,
|
|
})
|
|
} finally {
|
|
performance.now = originalNow
|
|
pack.dispose()
|
|
backMaterial.dispose()
|
|
renderer.dispose()
|
|
renderer.forceContextLoss()
|
|
}
|
|
}
|
|
return { passed: results.every((result) => result.passed),
|
|
durationMs: Math.round(performance.now() - startedAt), results }
|
|
} finally {
|
|
for (const texture of [...textures, environment]) texture.dispose()
|
|
}
|
|
}
|