File size: 3,549 Bytes
31c7d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import {
  BackgroundRemovalRequiredError,
  CancelledError,
} from '@ai3d/triposplat-webgpu'
import {
  ImageToSplatGenerator,
  type ImageToSplatResult,
} from './image-to-splat'

const imageInput = document.querySelector<HTMLInputElement>('#image')!
const generateButton = document.querySelector<HTMLButtonElement>('#generate')!
const cancelButton = document.querySelector<HTMLButtonElement>('#cancel')!
const plyButton = document.querySelector<HTMLButtonElement>('#download-ply')!
const splatButton = document.querySelector<HTMLButtonElement>('#download-splat')!
const status = document.querySelector<HTMLElement>('#status')!
const modelBaseUrl = document.querySelector<HTMLMetaElement>(
  'meta[name="triposplat-model-base-url"]',
)?.content

if (!modelBaseUrl) throw new Error('Set the triposplat-model-base-url meta tag.')

const generator = new ImageToSplatGenerator({
  modelBaseUrl,
  manifestUrl: 'manifest.json',
  executionProviders: ['webgpu'],
  cache: 'opfs',
})

let controller: AbortController | undefined
let result: ImageToSplatResult | undefined

function setResult(next?: ImageToSplatResult) {
  result = next
  plyButton.disabled = !next
  splatButton.disabled = !next
}

function download(blob: Blob, name: string) {
  const url = URL.createObjectURL(blob)
  const anchor = document.createElement('a')
  anchor.href = url
  anchor.download = name
  anchor.click()
  URL.revokeObjectURL(url)
}

const compatibility = await ImageToSplatGenerator.checkCompatibility()
if (!compatibility.supported) {
  status.textContent = compatibility.blockers.join('\n') || 'WebGPU is unavailable.'
  generateButton.disabled = true
} else {
  status.textContent = 'Ready. Select an alpha-bearing PNG or prepared image.'
}

cancelButton.addEventListener('click', () => controller?.abort())
plyButton.addEventListener('click', () => {
  if (result) download(result.ply, 'triposplat.ply')
})
splatButton.addEventListener('click', () => {
  if (result) download(result.splat, 'triposplat.splat')
})

generateButton.addEventListener('click', async () => {
  const file = imageInput.files?.[0]
  if (!file) {
    status.textContent = 'Choose an image first.'
    return
  }

  controller?.abort()
  controller = new AbortController()
  setResult()
  generateButton.disabled = true

  try {
    await generator.initialize({
      signal: controller.signal,
      onProgress: (event) => { status.textContent = event.message },
    })
    const next = await generator.generate(file, {
      steps: 4,
      gaussianCount: 262144,
      seed: 42,
      signal: controller.signal,
      onProgress: (event) => { status.textContent = event.message },
    })
    setResult(next)
    status.textContent = [
      `Generated ${next.count.toLocaleString()} Gaussians.`,
      `PLY: ${(next.ply.size / 1_048_576).toFixed(1)} MiB`,
      `.splat: ${(next.splat.size / 1_048_576).toFixed(1)} MiB`,
      `Elapsed: ${(next.elapsedMs / 1000).toFixed(1)} s`,
    ].join('\n')
  } catch (error) {
    if (error instanceof CancelledError) {
      status.textContent = 'Cancelled. The next run will create a clean worker.'
    } else if (error instanceof BackgroundRemovalRequiredError) {
      status.textContent = 'This opaque image needs a local background remover. Use an alpha-bearing PNG.'
    } else {
      status.textContent = error instanceof Error ? error.message : String(error)
    }
  } finally {
    generateButton.disabled = false
  }
})

window.addEventListener('pagehide', () => {
  controller?.abort()
  void generator.dispose()
}, { once: true })