feat: add model stats helper to viewer

This commit is contained in:
Tom Boullay
2026-04-27 17:04:38 +02:00
parent 3244b70bbf
commit d9c47c16eb
2 changed files with 114 additions and 6 deletions
+82 -4
View File
@@ -4,10 +4,18 @@ import { Suspense, useEffect } from 'react'
import { Canvas } from '@react-three/fiber'
import { Stage, OrbitControls } from '@react-three/drei'
import { useLoader } from '@react-three/fiber'
import type { Material, Mesh, Texture } from 'three'
import type { Material, Mesh, Object3D, Texture } from 'three'
import { TextureLoader } from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
export interface ModelStats {
drawCalls: number
materials: number
meshes: number
textures: number
triangles: number
}
interface OpacityMapEntry {
target: string
url: string
@@ -53,6 +61,56 @@ function applyAlphaMap(material: Material, texture: Texture) {
material.needsUpdate = true
}
function countGeometryTriangles(mesh: Mesh) {
const geometry = mesh.geometry
if (geometry.index) return Math.floor(geometry.index.count / 3)
const position = geometry.getAttribute('position')
return position ? Math.floor(position.count / 3) : 0
}
function getMaterialCount(material: Material | Material[]) {
return Array.isArray(material) ? material.length : 1
}
function estimateMeshDrawCalls(mesh: Mesh) {
if (mesh.geometry.groups.length > 0) return mesh.geometry.groups.length
return getMaterialCount(mesh.material)
}
function getTextureCount(assetUrls: Record<string, string>) {
return Object.keys(assetUrls).filter((filename) => /\.(png|jpe?g|webp)$/i.test(filename)).length
}
function getModelStats(scene: Object3D, assetUrls: Record<string, string>): ModelStats {
const materials = new Set<Material>()
let drawCalls = 0
let meshes = 0
let triangles = 0
scene.traverse((object) => {
const mesh = object as Mesh
if (!mesh.isMesh || !mesh.geometry || !mesh.material) return
meshes += 1
triangles += countGeometryTriangles(mesh)
drawCalls += estimateMeshDrawCalls(mesh)
const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
meshMaterials.forEach((material) => materials.add(material))
})
return {
drawCalls,
materials: materials.size,
meshes,
textures: getTextureCount(assetUrls),
triangles,
}
}
function pickOpacityMap(
mesh: Mesh,
material: Material,
@@ -73,13 +131,25 @@ function pickOpacityMap(
return entries.length === 1 ? textures[0] : undefined
}
function Model({ url, assetUrls }: { url: string; assetUrls: Record<string, string> }) {
function Model({
url,
assetUrls,
onStatsReady,
}: {
url: string
assetUrls: Record<string, string>
onStatsReady: (stats: ModelStats) => void
}) {
const { scene } = useLoader(GLTFLoader, url, (loader) => {
loader.manager.setURLModifier((requestedUrl) => resolveAssetUrl(requestedUrl, assetUrls))
})
const opacityMapEntries = getOpacityMapEntries(assetUrls)
const opacityMaps = useLoader(TextureLoader, opacityMapEntries.map((entry) => entry.url)) as Texture[]
useEffect(() => {
onStatsReady(getModelStats(scene, assetUrls))
}, [assetUrls, onStatsReady, scene])
useEffect(() => {
if (opacityMapEntries.length === 0) return
@@ -98,12 +168,20 @@ function Model({ url, assetUrls }: { url: string; assetUrls: Record<string, stri
return <primitive object={scene} />
}
export default function SceneViewer({ url, assetUrls }: { url: string; assetUrls: Record<string, string> }) {
export default function SceneViewer({
url,
assetUrls,
onStatsReady,
}: {
url: string
assetUrls: Record<string, string>
onStatsReady: (stats: ModelStats) => void
}) {
return (
<Canvas dpr={[1, 2]} camera={{ fov: 50 }}>
<Suspense fallback={null}>
<Stage environment="city" intensity={0.6} adjustCamera={1.2}>
<Model url={url} assetUrls={assetUrls} />
<Model url={url} assetUrls={assetUrls} onStatsReady={onStatsReady} />
</Stage>
</Suspense>
<OrbitControls makeDefault autoRotate autoRotateSpeed={0.5} />