# ASCII Renderer

> Take any 3D model and render it as ASCII art

A shadcn/ui block from the [@uicapsule](https://uicapsule.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uicapsule/ascii-renderer).

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/ascii-renderer
- Screenshot: https://shadcn.exchange/shot/uicapsule/ascii-renderer/light.png
- Registry item: https://uicapsule.com/r/ascii-renderer.json
- Categories: effects, pixel-art
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uicapsule/ascii-renderer.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@react-three/drei`, `@react-three/fiber`, `three`

## Files

### `/ascii-renderer.tsx`

```tsx
"use client";

import { useEffect, useRef } from "react";
import { OrbitControls } from "@react-three/drei";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import type { Mesh } from "three";
import { AsciiEffect } from "three/examples/jsm/effects/AsciiEffect.js";

const Torusknot = () => {
  const meshRef = useRef<Mesh>(null);

  useFrame((_state, delta) => {
    const mesh = meshRef.current;
    if (!mesh) {
      return;
    }
    mesh.rotation.y += delta / 2;
    mesh.rotation.x = mesh.rotation.y;
  });

  return (
    <mesh ref={meshRef} scale={1.25}>
      <torusKnotGeometry args={[1, 0.2, 128, 32]} />
      <meshStandardMaterial color="#ffffff" />
    </mesh>
  );
};

const Renderer = () => {
  const { gl, scene, camera, size } = useThree();
  const effectRef = useRef<AsciiEffect>(null);

  useEffect(() => {
    const effect = new AsciiEffect(gl, " .:-+*=%@#");

    effect.domElement.style.position = "absolute";
    effect.domElement.style.top = "0px";
    effect.domElement.style.left = "0px";
    effect.domElement.style.color = "white";
    effect.domElement.style.backgroundColor = "black";
    effect.domElement.style.pointerEvents = "none";

    const container = gl.domElement.parentNode;
    if (container) {
      gl.domElement.replaceWith(effect.domElement);
    }

    effectRef.current = effect;

    return () => {
      effectRef.current = null;
      if (container && effect.domElement.parentNode) {
        effect.domElement.replaceWith(gl.domElement);
      }
    };
  }, [gl]);

  // Resizing must not tear the effect down: recreating it would detach and
  // reattach the ASCII <div>, dropping a frame on every resize tick.
  useEffect(() => {
    effectRef.current?.setSize(size.width, size.height);
  }, [size.width, size.height]);

  useFrame(() => {
    if (effectRef.current) {
      effectRef.current.render(scene, camera);
    }
  }, 1);

  return null;
};

export const AsciiRenderer = () => (
  <Canvas>
    <ambientLight intensity={0.5} />
    <pointLight position={[10, 10, 10]} intensity={1} />
    <directionalLight position={[-10, -10, -5]} intensity={1} />
    <Torusknot />
    <OrbitControls />
    <Renderer />
  </Canvas>
);
```

### `/preview.tsx`

```tsx
"use client";

import { AsciiRenderer } from "./ascii-renderer";

const Preview = () => <AsciiRenderer />;

export default Preview;
```
