feat: add model stats helper to viewer
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import type { ModelStats } from './SceneViewer'
|
||||||
|
|
||||||
interface ModelViewerProps {
|
interface ModelViewerProps {
|
||||||
url: string
|
url: string
|
||||||
@@ -11,7 +12,12 @@ interface ModelViewerProps {
|
|||||||
|
|
||||||
export default function ModelViewer({ url, assetUrls, filename, size }: ModelViewerProps) {
|
export default function ModelViewer({ url, assetUrls, filename, size }: ModelViewerProps) {
|
||||||
const canPreview = filename.toLowerCase().endsWith('.gltf')
|
const canPreview = filename.toLowerCase().endsWith('.gltf')
|
||||||
const [Scene, setScene] = useState<React.ComponentType<{ url: string; assetUrls: Record<string, string> }> | null>(null)
|
const [stats, setStats] = useState<ModelStats | null>(null)
|
||||||
|
const [Scene, setScene] = useState<React.ComponentType<{
|
||||||
|
url: string
|
||||||
|
assetUrls: Record<string, string>
|
||||||
|
onStatsReady: (stats: ModelStats) => void
|
||||||
|
}> | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canPreview) return
|
if (!canPreview) return
|
||||||
@@ -53,7 +59,31 @@ export default function ModelViewer({ url, assetUrls, filename, size }: ModelVie
|
|||||||
{size}
|
{size}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Scene url={url} assetUrls={assetUrls} />
|
{stats && (
|
||||||
|
<div className="absolute bottom-3 left-3 right-3 z-10 grid grid-cols-2 gap-2 rounded-lg border border-white/10 bg-black-900/75 p-2 text-xs text-gray-300 backdrop-blur sm:left-auto sm:right-3 sm:w-64">
|
||||||
|
<span className="flex justify-between gap-3">
|
||||||
|
<span className="text-gray-500">Draw calls</span>
|
||||||
|
<span className="font-mono text-gray-200">{stats.drawCalls}</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex justify-between gap-3">
|
||||||
|
<span className="text-gray-500">Meshes</span>
|
||||||
|
<span className="font-mono text-gray-200">{stats.meshes}</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex justify-between gap-3">
|
||||||
|
<span className="text-gray-500">Triangles</span>
|
||||||
|
<span className="font-mono text-gray-200">{stats.triangles.toLocaleString('fr-FR')}</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex justify-between gap-3">
|
||||||
|
<span className="text-gray-500">Materials</span>
|
||||||
|
<span className="font-mono text-gray-200">{stats.materials}</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex justify-between gap-3">
|
||||||
|
<span className="text-gray-500">Textures</span>
|
||||||
|
<span className="font-mono text-gray-200">{stats.textures}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Scene url={url} assetUrls={assetUrls} onStatsReady={setStats} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ import { Suspense, useEffect } from 'react'
|
|||||||
import { Canvas } from '@react-three/fiber'
|
import { Canvas } from '@react-three/fiber'
|
||||||
import { Stage, OrbitControls } from '@react-three/drei'
|
import { Stage, OrbitControls } from '@react-three/drei'
|
||||||
import { useLoader } from '@react-three/fiber'
|
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 { TextureLoader } from 'three'
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
|
|
||||||
|
export interface ModelStats {
|
||||||
|
drawCalls: number
|
||||||
|
materials: number
|
||||||
|
meshes: number
|
||||||
|
textures: number
|
||||||
|
triangles: number
|
||||||
|
}
|
||||||
|
|
||||||
interface OpacityMapEntry {
|
interface OpacityMapEntry {
|
||||||
target: string
|
target: string
|
||||||
url: string
|
url: string
|
||||||
@@ -53,6 +61,56 @@ function applyAlphaMap(material: Material, texture: Texture) {
|
|||||||
material.needsUpdate = true
|
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(
|
function pickOpacityMap(
|
||||||
mesh: Mesh,
|
mesh: Mesh,
|
||||||
material: Material,
|
material: Material,
|
||||||
@@ -73,13 +131,25 @@ function pickOpacityMap(
|
|||||||
return entries.length === 1 ? textures[0] : undefined
|
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) => {
|
const { scene } = useLoader(GLTFLoader, url, (loader) => {
|
||||||
loader.manager.setURLModifier((requestedUrl) => resolveAssetUrl(requestedUrl, assetUrls))
|
loader.manager.setURLModifier((requestedUrl) => resolveAssetUrl(requestedUrl, assetUrls))
|
||||||
})
|
})
|
||||||
const opacityMapEntries = getOpacityMapEntries(assetUrls)
|
const opacityMapEntries = getOpacityMapEntries(assetUrls)
|
||||||
const opacityMaps = useLoader(TextureLoader, opacityMapEntries.map((entry) => entry.url)) as Texture[]
|
const opacityMaps = useLoader(TextureLoader, opacityMapEntries.map((entry) => entry.url)) as Texture[]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onStatsReady(getModelStats(scene, assetUrls))
|
||||||
|
}, [assetUrls, onStatsReady, scene])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opacityMapEntries.length === 0) return
|
if (opacityMapEntries.length === 0) return
|
||||||
|
|
||||||
@@ -98,12 +168,20 @@ function Model({ url, assetUrls }: { url: string; assetUrls: Record<string, stri
|
|||||||
return <primitive object={scene} />
|
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 (
|
return (
|
||||||
<Canvas dpr={[1, 2]} camera={{ fov: 50 }}>
|
<Canvas dpr={[1, 2]} camera={{ fov: 50 }}>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Stage environment="city" intensity={0.6} adjustCamera={1.2}>
|
<Stage environment="city" intensity={0.6} adjustCamera={1.2}>
|
||||||
<Model url={url} assetUrls={assetUrls} />
|
<Model url={url} assetUrls={assetUrls} onStatsReady={onStatsReady} />
|
||||||
</Stage>
|
</Stage>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<OrbitControls makeDefault autoRotate autoRotateSpeed={0.5} />
|
<OrbitControls makeDefault autoRotate autoRotateSpeed={0.5} />
|
||||||
|
|||||||
Reference in New Issue
Block a user