About
An orbit viewer for generated 3D assets on a small hand written WebGL 2 renderer. Matcap and wireframe shading, drag to orbit, throw and zoom, keyboard controls and a turntable export to video.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds 3D Asset Viewer to your project and fits it to your theme.
Code
How 3D Asset Viewer is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client"
import * as React from "react"
import { AssetMesh, AssetViewer, normalizeMesh } from "@/components/ui/3d-asset-viewer"
/* -------------------------------------------------------------------------------------------------
* Procedural assets
* -----------------------------------------------------------------------------------------------*/
/**
* Spins a profile (`[radius, height]` pairs from bottom to top) around the vertical axis: vases,
* bottles, lamps, mushrooms. Caps the bottom when it starts away from the axis.
*/
function latheMesh(
profile: readonly (readonly [number, number])[],
segments = 48,
): AssetMesh {
const rows = profile.length
const pos: number[] = []
const idx: number[] = []
for (let r = 0; r < rows; r++) {
const [radius, y] = profile[r] as readonly [number, number]
for (let s = 0; s <= segments; s++) {
const a = (s / segments) * Math.PI * 2
pos.push(Math.cos(a) * radius, y, Math.sin(a) * radius)
}
}
const w = segments + 1
for (let r = 0; r < rows - 1; r++) {
for (let s = 0; s < segments; s++) {
const a = r * w + s
const b = a + w
idx.push(a, b, a + 1, a + 1, b, b + 1)
}
}
const [r0, y0] = profile[0] as readonly [number, number]
if (r0 > 0) {
const center = pos.length / 3
pos.push(0, y0, 0)
for (let s = 0; s < segments; s++) idx.push(center, s + 1, s)
}
const mesh = { positions: new Float32Array(pos), indices: new Uint32Array(idx) }
return normalizeMesh(mesh)
}
/** A short table vase. */
const VASE_PROFILE: readonly (readonly [number, number])[] = [
[0.0, 0],
[0.3, 0],
[0.34, 0.03],
[0.42, 0.2],
[0.45, 0.38],
[0.38, 0.58],
[0.24, 0.76],
[0.2, 0.88],
[0.24, 0.98],
[0.26, 1],
[0.22, 1],
[0.18, 0.9],
]
/** A generated ceramic vase on the turntable. Drag to orbit, switch to wireframe, export a turn. */
function AssetViewerDemo() {
const vase = React.useMemo(() => latheMesh(VASE_PROFILE, 56), [])
return (
<div className="w-full max-w-xl">
<AssetViewer
mesh={vase}
alt="Ceramic table vase with a rounded belly and a narrow lipped neck"
defaultMode="both"
/>
</div>
)
}
/** The line under each 21st demo that points back to the kit. */
function MoreOn({ slug, children }: { slug: string; children: React.ReactNode }) {
return (
<p className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint) text-center text-balance">
{children}{" "}
<a
href={`https://chatcn.dev/docs/components/${slug}`}
target="_blank"
rel="noopener"
className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
>
chatcn.dev
</a>
</p>
)
}
export default function AssetViewerPreview() {
return (
<div className="flex min-h-[520px] w-full flex-col items-center justify-center gap-10 p-6">
<AssetViewerDemo />
<MoreOn slug="3d-asset-viewer">
Drag to orbit, switch to wireframe, export a turntable. More on
</MoreOn>
</div>
)
}















