Render a 3D Shape in ASCII

Build a Three.js shader that prints a spinning 3D shape as shape-aware ASCII.

Introduction

30 Second Summary

When pictures are reduced to light and dark characters, smooth curves often collapse into smudgy blocks.

In this project, you will build a browser page that uses Three.js to print a spinning 3D solid as shape-matched ASCII. You will compare the result with a brightness-only pass to see how shape matching preserves sharper edges.

What You'll Build

Picture a spinning solid rendered as readable characters beside a softer brightness-only version.

By the end of this project, you'll have:

  • A live browser demo where a lit 3D solid spins as readable ASCII characters.
  • A visible glyph atlas that lets you inspect every character available to the renderer.
  • A side-by-side comparison that shows how shape-aware matching keeps edges sharper than a brightness-only pass.
  • Secret Mission: An optional challenge to push your skills further.

Are there any prerequisites?

You need Node.js, a modern browser, and enough JavaScript to follow a render loop. The shader syntax is provided for you to copy and modify.

Before We Start

This browser project asks your computer to build the page locally before it displays the 3D scene. A small toolchain keeps that feedback loop fast.

Node.js runs Vite on your computer. npm supplies the packages the project needs.

Your code editor holds the project files. Your browser shows every visual result.

In this step, get ready to:
  • Prepare a code editor for your project files.
  • Prepare a modern browser for viewing the rendered scene.
  • Confirm Node.js and npm are available from your terminal.
Prepare your editor and browser

Your editor holds the code. Your browser displays the page created from that code.

  • Install a code editor from its official website if your computer does not have one.
  • Launch your code editor through your operating system's search.

You'll see your editor's main window ready for a folder. This is the workspace you'll use for the project files.

  • Install a modern browser from its official website if your computer does not have one.
  • Launch your browser through your operating system's search.

You'll see a browser window with a usable tab. This is where your ASCII renderer appears as you build it.

Check Node.js and npm

The official Node.js installer includes npm. A version response from each tool confirms your computer can run the project tooling.

  • Open a terminal window through your operating system's search.

Before you run this check, decide whether both tools are already available on your computer.

  • Check both tool versions by running these commands:
node --version
npm --version

What do these commands check?

  • The node --version command asks Node.js to report its installed version.
  • The npm --version command asks npm to report its installed version.
  • Two version responses confirm that both parts of your local toolchain are available.

✔️ I see two version numbers

You're set. The two version numbers prove Node.js and npm are available in this terminal.

ⓧ A command is missing

A missing command means Node.js needs to be installed again. The official installer adds npm during the same setup.

You'll see the installer file in your browser's downloads. It contains the runtime files your computer needs.

  • Run the downloaded installer.

Your operating system may ask for permission during installation. That approval lets the official installer add Node.js to your computer.

  • Approve the operating system permission prompt if it appears.
  • Complete the setup using the installer defaults.

You'll see a setup completion message when the installation finishes.

  • Reopen a terminal window through your operating system's search.
  • Verify the installation by running these commands:
node --version
npm --version

What confirms the installation?

The first command checks the Node.js runtime. The second command checks the npm package manager.

A version response from both commands confirms the installer added both tools successfully.

You'll now see two version numbers in your terminal. Your Node.js installation is ready.

Still missing a command?

  • Close every terminal window so the new installation can be detected.
  • Reopen your terminal through your operating system's search.
  • Run the official installer again if either command remains unavailable.

Help me troubleshoot a missing Node.js or npm command.

That's the groundwork finished. Your computer is ready for the project files.

Your full local toolchain is ready. Next, you'll create the Vite page that becomes your ASCII renderer.

Create a Spinning Three.js Solid

Your ASCII effect needs a clear 3D image before it can turn that image into characters. This step creates the moving source scene that every later rendering pass uses.

A Vite project gives you a local development server. Three.js supplies the scene, camera, renderer, solid, lighting, and animation tools.

In this step, get ready to:
  • Prepare a local Vite project with Three.js.
  • Render a lit 3D solid in the browser.
  • Animate the solid while keeping the canvas responsive.
Scaffold the Vite project

Vite creates the page entry point plus the scripts needed for local development. The vanilla template keeps the project focused on JavaScript.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The command moves your terminal into the Desktop directory. Your new project folder appears somewhere easy to find.

  • Create the vanilla JavaScript project by running this command:
npm create vite@latest ascii-solid -- --template vanilla

What did Vite create?

  • The command creates an ascii-solid folder on your Desktop.
  • The vanilla template adds index.html as the page entry point.
  • The template adds src/main.js for your application code.
  • The generated package.json includes development and production scripts.

Good start. Your browser project now has the standard Vite structure that the 3D scene needs.

Project folder missing?

  • Check that your terminal is still inside the Desktop directory.
  • Confirm that your internet connection allows npm to download the Vite scaffolding package.
  • Remove any existing ascii-solid folder before retrying the scaffold command.
  • Help me troubleshoot my Vite project creation.
  • Move into the new project folder by running this command:
cd ascii-solid

Why move into the folder?

The next npm commands read the package.json file in the current directory. Moving into ascii-solid makes every dependency part of this project.

  • Install the project dependencies by running these commands:
npm install
npm install three

What do these commands install?

  • The first command installs the packages declared by the Vite template.
  • The second command adds Three.js to the project dependencies.
  • The generated lock file records the installed dependency versions.

The terminal finishes without an installation error. Three.js is now available to import from src/main.js.

Dependency installation failing?

  • Confirm that the terminal path ends with ascii-solid.
  • Check your internet connection before retrying the installation.
  • Read the final lines of the npm output for the package that failed.
  • Help me troubleshoot my Three.js installation.
  • Start the Vite development server by running this command:
npm run dev

What does the development server do?

Vite serves the project through a local URL printed in your terminal. It also updates the browser when you save application code.

  • Open the local URL printed in your terminal.

You should see the vanilla Vite starter page. That page proves the project structure can load through the development server.

Local page not loading?

  • Keep the terminal process running while the browser loads the page.
  • Use the exact local URL printed by Vite in case another process changed the port.
  • Check that a firewall prompt is not waiting for your response.
  • Help me troubleshoot my Vite development server.
  • Leave the development server running while you build the scene.
Build the lit scene

A 3D scene needs a camera to define the view. A WebGL renderer turns that view into pixels on a canvas.

  • Use your code editor's folder picker to select the ascii-solid folder on your Desktop.

The file sidebar should now show index.html plus the src folder. This confirms that your editor is using the scaffolded project.

  • Open src/main.js from the file sidebar.
  • Replace the file contents by pasting this code:
import * as THREE from 'three';

// Create the world and define the camera view.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// Create a full-window canvas for the scene.
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Build the solid from geometry and a light-reactive material.
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x44aa88 });
const solid = new THREE.Mesh(geometry, material);
scene.add(solid);

// Add soft base light and a stronger directional highlight.
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

// Draw the first frame before animation starts.
renderer.render(scene, camera);

What does this code do?

  • The scene stores everything Three.js needs to draw.
  • The camera defines the visible area from a position in front of the solid.
  • The solid combines box geometry with a material that reacts to light.
  • The ambient light exposes the shaded faces. The directional light adds a brighter highlight.
  • Save src/main.js.
  • Switch to the browser tab running your Vite page.

You should see a green cube with bright and shaded faces. The visible shading proves that the material responds to the lights.

Solid not visible?

  • Confirm that src/main.js starts with the Three.js import.
  • Check that the browser is still using the local URL printed by Vite.
  • Compare the camera position with the code above if the canvas stays blank.
  • Help me find my missing Three.js solid.
Animate and resize the solid

The static render proves that the scene works. An animation loop redraws that scene after changing the solid's rotation.

  • In src/main.js, find the final renderer.render(scene, camera); line.
  • Add this animation code below that line:
// Rotate the solid before drawing every animation frame.
function animate() {
  solid.rotation.x += 0.01;
  solid.rotation.y += 0.01;
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

How does the animation work?

  • The animate() function adjusts the solid's rotation before rendering each frame.
  • The two rotation values move at the same steady rate across different axes.
  • The animation loop asks the renderer to repeat that work while the page remains active.
  • Save src/main.js.
  • Switch back to the browser tab.

The cube should now rotate continuously. That moving highlight makes its 3D form easy to read.

Cube staying still?

  • Confirm that the animation code appears after the first render call.
  • Check that both rotation lines use the existing solid variable.
  • Look for a syntax error in the browser console.
  • Help me fix my Three.js animation loop.

The canvas also needs to follow the browser dimensions. Updating the camera's aspect ratio prevents the solid from stretching when the window changes shape.

  • Add this resize handler below the animation loop in src/main.js.
// Keep the canvas and camera aligned with the browser window.
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

Why update the camera?

  • The resize event runs whenever the browser viewport changes dimensions.
  • The camera receives the new width-to-height ratio.
  • The projection matrix applies that ratio to the next frame.
  • The renderer resizes its canvas to fill the new viewport.
  • Save src/main.js.

Before you check the final result, do you expect the cube to stretch or keep its proportions when the browser becomes narrow?

  • Resize the browser window from wide to narrow.

You should see the lit cube continue spinning without stretching. That confirms the scene, camera, renderer, lights, animation loop, and resize handling are working together.

Solid stretching after resize?

  • Confirm that the resize handler updates camera.aspect.
  • Check that camera.updateProjectionMatrix() appears after the aspect update.
  • Verify that the renderer receives the new browser dimensions.
  • Help me fix my responsive Three.js canvas.

✔️ Awesome, I've got everything!

Your Vite server is running. Double check that the browser shows a lit cube that keeps spinning as you resize the window.

ⓧ I'd like to double check the full code

Compare your complete src/main.js file with this version:

import * as THREE from 'three';

// Create the world and define the camera view.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// Create a full-window canvas for the scene.
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Build the solid from geometry and a light-reactive material.
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x44aa88 });
const solid = new THREE.Mesh(geometry, material);
scene.add(solid);

// Add soft base light and a stronger directional highlight.
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

// Draw the first frame before animation starts.
renderer.render(scene, camera);

// Rotate the solid before drawing every animation frame.
function animate() {
  solid.rotation.x += 0.01;
  solid.rotation.y += 0.01;
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

// Keep the canvas and camera aligned with the browser window.
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

What should the full file contain?

The file imports Three.js before creating the scene, camera, renderer, solid, and lights. Its final sections animate the solid and keep the canvas aligned with the browser window.

Your source scene is live and responsive. Next up, you'll draw that same scene into a render target before displaying it on the page.

Render the Scene to a Texture

Your lit solid is already rotating in the browser through Three.js. The ASCII effect needs each finished frame in a form that another shader can sample.

This step sends the scene through a render target before it reaches the canvas. A full-screen pass presents the resulting texture without changing how the solid looks.

In this step, get ready to:
  • Create an offscreen render target at the browser viewport size.
  • Display the render target texture through a full-screen pass.
  • Keep the offscreen texture aligned with browser resizing.
Create the offscreen display pass

A WebGLRenderTarget gives the renderer an offscreen color texture. A second scene holds a full-screen quad that copies this texture onto the visible canvas.

  • Switch back to src/main.js in your code editor.
  • Locate the section where your renderer receives its initial size.
  • Add the texture target and display pass immediately below that section by copying this code:
// Store each rendered frame in an offscreen color texture.
const renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight)

// Build a separate scene for presenting the finished texture.
const screenScene = new THREE.Scene()
const screenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
// Copy the offscreen texture across the visible canvas.
const screenMaterial = new THREE.ShaderMaterial({
  uniforms: { tDiffuse: { value: renderTarget.texture } },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    varying vec2 vUv;
    void main() {
      gl_FragColor = texture2D(tDiffuse, vUv);
    }
  `,
  depthTest: false,
  depthWrite: false,
})

// Fill the full clip-space area with the texture.
const screenQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), screenMaterial)
screenScene.add(screenQuad)

What does this code do?

  • The renderTarget stores a color texture with the same width and height as the browser viewport.
  • The screenCamera views coordinates that cover the entire canvas.
  • The screenMaterial samples the offscreen texture through the tDiffuse uniform.
  • The screenQuad fills the screen with that sampled image.
  • Save src/main.js.
  • Return to the browser from earlier.

You should still see the lit solid rotating. This confirms the new display-pass objects were created without interrupting the existing scene.

Did the page stop rendering?

Check that the new code sits after the Three.js import. Confirm that every opening bracket and template-string backtick has a matching closing character.

Use this prompt if the browser remains blank: Help me find the syntax problem in my Three.js render target and full-screen shader setup.

Route each frame through the texture

The new target exists in memory. The animation loop now needs two render passes for every frame.

  • Find the direct render call inside the animation loop in src/main.js.
  • Replace that direct render call with this two-pass sequence:
// Draw the lit 3D scene into the offscreen texture.
renderer.setRenderTarget(renderTarget)
renderer.render(scene, camera)

// Return to the canvas and present the texture full screen.
renderer.setRenderTarget(null)
renderer.render(screenScene, screenCamera)

How do the two passes work?

  • The first setRenderTarget() call sends the next render into renderTarget.
  • Passing null switches the renderer back to the browser canvas.
  • The final render draws screenScene so the stored texture fills the visible page.

Before you save this change, what do you expect the canvas to show? Hold that prediction for the next check.

  • Save src/main.js.
  • Return to the browser from earlier.

You should see the same lit solid rotating at the same size. The matching picture proves that each frame reaches the canvas through the offscreen texture.

That is the rendering bridge complete. Your scene now produces a texture that later shader passes can inspect.

Seeing a blank canvas?

Confirm that renderer.setRenderTarget(null) runs before the screen scene renders. Without that reset, the second pass keeps drawing into the offscreen target.

Check that the screen material reads from renderTarget.texture. A different value leaves the quad without an image to display.

Use this prompt for another set of eyes: Help me debug my blank full-screen texture pass.

Keep the texture matched to the viewport

The browser canvas can change size after the render target is created. Resizing both surfaces together prevents stretching and stale pixels around the edges.

  • Locate the existing browser resize handler in src/main.js.
  • Add this line after the renderer receives its new size:
// Keep the offscreen texture aligned with the visible canvas.
renderTarget.setSize(window.innerWidth, window.innerHeight)

Why resize the render target?

The canvas and render target hold separate pixel buffers. Calling renderTarget.setSize() keeps their dimensions aligned whenever the viewport changes.

Before you resize the browser, how do you expect the solid to behave near the canvas edges? Keep that prediction in mind during the check.

  • Save src/main.js.
  • Resize the browser by dragging one edge of its window.

You should see the solid keep its proportions while the texture continues to fill the canvas. No stretched image or uncovered strip should appear.

Does the texture stretch after resizing?

Make sure the render target resize line is inside the existing resize handler. Confirm that it uses the same viewport dimensions as the renderer.

Ask for targeted help here: Help me keep my Three.js render target aligned with the browser canvas.

✔️ Awesome, I've got everything!

Great. Your rotating solid now reaches the canvas through a viewport-sized render target while keeping its original appearance.

ⓧ I'd like to double check the full code

Keep your existing scene setup and solid code unchanged. Compare the texture-pass sections in src/main.js with these snippets.

// Store each rendered frame in an offscreen color texture.
const renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight)

// Build a separate scene for presenting the finished texture.
const screenScene = new THREE.Scene()
const screenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
// Copy the offscreen texture across the visible canvas.
const screenMaterial = new THREE.ShaderMaterial({
  uniforms: { tDiffuse: { value: renderTarget.texture } },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    varying vec2 vUv;
    void main() {
      gl_FragColor = texture2D(tDiffuse, vUv);
    }
  `,
  depthTest: false,
  depthWrite: false,
})

// Fill the full clip-space area with the texture.
const screenQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), screenMaterial)
screenScene.add(screenQuad)
// Draw the lit 3D scene into the offscreen texture.
renderer.setRenderTarget(renderTarget)
renderer.render(scene, camera)

// Return to the canvas and present the texture full screen.
renderer.setRenderTarget(null)
renderer.render(screenScene, screenCamera)
// Keep the offscreen texture aligned with the visible canvas.
renderTarget.setSize(window.innerWidth, window.innerHeight)

Your scene now exists as a texture that another shader can read. Next up, you will build a visible glyph atlas for the characters that print the final image.

Build and Display a Glyph Atlas

Your Three.js scene now reaches the screen through a WebGLRenderTarget. That offscreen image gives the ASCII pass its source pixels.

The next pass also needs a glyph atlas. A glyph atlas is a grid of pre-rendered characters.

You will expose the atlas to Three.js as a CanvasTexture. You will also keep the source canvas visible for inspection.

In this step, get ready to:
  • Lay out the complete character set in equal canvas cells.
  • Turn the atlas canvas into a Three.js texture.
  • Inspect every glyph in the browser.
Lay out the glyph grid

Each character needs a stable position inside the atlas. Equal cells let the next shader find any glyph from its character index.

How does the atlas grid work?

  • The character set fixes the order that later shader indices use.
  • A monospaced font gives each glyph consistent spacing.
  • A fixed cell width keeps the columns aligned.
  • A fixed cell height keeps the rows aligned.
  • The canvas dimensions come from the character count plus the fixed cell dimensions.
  • Switch back to the code editor from earlier.
  • Add a visible atlas preview area to index.html.
  • Save index.html.

Your Vite page reloads. You'll see the page reserve a separate region for the atlas beneath the rendered scene.

  • Return to src/main.js in the code editor from earlier.
  • Implement the visible atlas canvas using the grid requirements above.
  • Save src/main.js.

Your browser updates after the save. You'll see the complete chosen character set arranged in a regular grid.

Is the atlas missing or clipped?

Confirm that the generated canvas is attached to the preview area after its dimensions are set. Check that the selected font fits inside the fixed cell height.

If characters overlap across columns, increase the fixed cell width or reduce the font size.

Help me debug my glyph atlas layout.

Create the atlas texture

The visible preview confirms that the browser drew the characters correctly. A Three.js CanvasTexture makes those same pixels available to shader materials.

  • In src/main.js from earlier, create a Three.js CanvasTexture beside the other rendering resources.
  • Save src/main.js.

Your page reloads without a rendering error. You'll still see the rotating solid plus the glyph atlas preview.

Why keep the source canvas visible?

The texture gives shaders access to the atlas. The visible canvas gives you a direct way to catch missing glyphs or uneven spacing before shader sampling hides the cause.

Did the page stop rendering?

Make sure the atlas canvas exists before you use it to create the texture. A texture cannot read from a canvas that has not been created yet.

Check the browser console for the first reported error. Fix that line before refreshing the page again.

Help me fix my atlas texture setup.

Inspect every glyph

The preview is your quality check for the atlas. Clipped characters or uneven cells would give the next shader the wrong glyph shapes.

Before you refresh the page, which glyphs do you expect to reveal spacing or clipping problems first?

  • Refresh your browser.
  • Scan the atlas from its first cell to its last cell.

You'll see every character from the chosen set inside its own fixed-size cell. The rows will stay aligned across the full atlas.

That is a fiddly rendering step complete. Your page now carries a readable glyph library that shader materials can sample.

Your glyph atlas is visible as a canvas plus available as a texture. Next, you'll use cell brightness to select characters from it and print the solid in ASCII.

Print the Scene with Brightness-Matched Glyphs

Your rotating solid already reaches the browser through an offscreen texture. Your visible glyph atlas provides the character shapes needed to redraw that texture as text.

The next pass divides the scene texture into character cells. A fragment shader uses each cell's average brightness to select one glyph from the atlas.

In this step, get ready to:
  • Map each character cell's brightness to a position in the existing character set.
  • Replace the full-screen texture display with a brightness-based ASCII shader.
  • Verify that the rotating solid remains readable after the browser canvas changes size.
Map brightness to the atlas

Brightness becomes a position in the ordered character set. Dark cells use sparse glyphs while bright cells use dense glyphs.

  • Return to the browser page from earlier.
  • Inspect the atlas preview to identify the glyph with the least filled area.
  • Inspect the atlas preview to identify the glyph with the most filled area.

How does brightness choose a glyph?

The shader measures every scene sample inside one character cell. It combines those samples into a single average brightness.

That brightness becomes a glyph index within the existing character set. The selected atlas cell supplies the shape printed across the output cell.

Build the brightness sampling pass

The full-screen pass needs enough information to locate a scene cell plus its matching atlas cell. Every fragment inside one output cell then uses the same brightness result.

What does the ASCII pass need?

  • The scene render texture supplies the color samples from the rotating solid.
  • The glyph atlas texture supplies the character masks.
  • The viewport resolution converts screen positions into character cells.
  • The glyph grid resolution locates each character inside the atlas.
  • The glyph cell dimensions control the size of each output character.
  • The character count converts average brightness into a valid glyph index.

The local position inside the output cell becomes a position inside the selected atlas glyph. Sampling that position stamps the glyph shape into the final image.

  • Switch back to src/main.js in your code editor.
  • Locate the full-screen texture pass that currently displays the render target.
  • Replace its display material with a custom full-screen shader material using the six inputs described above plus the brightness-to-glyph pipeline.
  • Keep the existing full-screen geometry unchanged.

Before you save, do you expect the solid's movement to survive its conversion into character cells?

  • Save src/main.js to trigger the running page update.

You should see the rotating solid printed with brightness-matched ASCII glyphs. Dark areas use sparser characters while illuminated areas use denser characters.

Seeing a blank or solid output?

  • Confirm that the scene input still uses the existing render target texture.
  • Confirm that the glyph input uses the existing atlas CanvasTexture.
  • Check that the brightness result stays within the available character range.

Share the relevant shader material plus the browser symptoms here: Help me debug my brightness-matched ASCII shader.

Keep the character grid stable

The viewport resolution uniform describes the canvas size used by the shader. Keeping it synchronized prevents character cells from stretching after a resize.

  • Return to the existing resize handler in src/main.js.
  • Update the viewport resolution uniform whenever the renderer plus the render target change size.

Before you resize the page, do you expect the character grid to stay aligned with the rotating solid?

  • Save src/main.js.
  • Resize the browser window by dragging one edge.

You should see the ASCII solid continue rotating with evenly aligned character cells. The glyph atlas preview remains visible on the page.

Do the glyphs stretch after resizing?

  • Confirm that the shader resolution updates inside the existing resize handler.
  • Confirm that the render target size updates from the same viewport dimensions.

Share your resize handler plus the stretched output here: Help me fix my ASCII grid after resizing.

You have the first text-rendered version working: the solid now spins as brightness-matched glyphs. Next, you will compare glyph shapes directly so the object's edges become sharper.

Match Glyph Shapes and Deploy the Page

Your brightness pass already turns the rotating Three.js solid into readable text. Glyphs with equal brightness can carry different strokes.

Now you will compare each scene-cell pattern with the shapes in your atlas. Vite then packages the finished comparison for a public deployment.

In this step, get ready to:
  • Create a side-by-side baseline from the existing brightness pass.
  • Replace the second pass's brightness lookup with glyph-shape scoring.
  • Publish a verified production build to a shareable URL.
Create a side-by-side baseline

A fair comparison gives both passes the same scene frame. It also gives them the same glyph atlas.

  • In the code editor from earlier, switch back to src/main.js.
  • Create a side-by-side copy of the brightness shader pass in src/main.js using the existing scene texture plus glyph atlas texture.
  • Save src/main.js.
  • Return to the local page from earlier.

You should see two matching ASCII renderings of the rotating solid. The glyph atlas should remain visible.

What does this comparison prove?

Both passes receive the same offscreen scene frame. Differences that appear after the next change therefore come from glyph selection.

The left pass stays unchanged. It remains your brightness baseline.

Score the glyph shapes

Each character cell contains a small pattern of light and dark samples. A shape-scored fragment shader chooses the glyph whose pattern has the smallest difference from that source pattern.

How does shape scoring work?

  • The shader samples brightness at fixed positions inside the current scene cell.
  • The shader reads the matching positions inside each candidate glyph.
  • Every sample difference is squared before being added to that candidate's score.
  • The glyph with the lowest total score becomes the winner for that scene cell.
  • In src/main.js, find the duplicated shader pass that feeds the right-hand view.
  • Replace its brightness-only glyph selection with the four-part shape-scoring sequence above.
  • Save src/main.js.
  • Return to the browser page from earlier.

The left view should still select glyphs from average brightness. The right view should use characters whose strokes follow the solid's edges more closely.

You have now made the visual difference measurable: both passes show the same frame while the shape-scored pass preserves sharper contours.

Does the right view look flat?

  • Check that the right pass samples the same render target texture as the left pass.
  • Confirm that source samples match the local positions used for each atlas glyph.
  • Check that the winning glyph index updates whenever a lower score is found.

Ask for help with the scoring loop.

Build and deploy the page

A production build bundles the page into static files inside dist. Netlify can publish that folder without a Git repository.

  • Create a second terminal panel in the code editor from earlier.
  • Build the production version from the existing Vite project folder by running:
npm run build

What does this command do?

Vite bundles the application into production-ready static files. The default output folder is dist.

You should see a successful build summary in the terminal. You should also see dist inside the Vite project folder.

Did the production build stop?

Read the first reported file location in the terminal. Build failures often reveal a shader syntax problem that the development page had not surfaced clearly.

Help me diagnose the failed Vite production build.

The preview server keeps its terminal occupied while it serves the production files. Leave that terminal running during the visual check.

Before you preview the build, do you expect both ASCII passes and the glyph atlas to survive the production bundle?

  • Start a local preview of the production build by running:
npm run preview

What does this command do?

Vite starts a local server that serves the files from dist. This lets you inspect the same bundle that you are about to deploy.

  • Open the local address printed in the terminal.

You should see the brightness-matched pass beside the sharper shape-scored pass. The rotating solid and visible glyph atlas should still work.

Is the production preview blank?

  • Confirm that the build completed before the preview server started.
  • Check the browser console for the first shader compilation problem.
  • Confirm that the atlas canvas still exists before its texture is created.

Help me fix the blank production preview.

Publishing makes the page publicly accessible. The dist folder contains the built site files that viewers need.

  • In your browser, go to Netlify Drop.
  • Complete the account prompt if Netlify asks you to sign in.
  • In your computer's file browser, locate the dist folder inside the Vite project folder.
  • Drag the dist folder onto the Drop zone.

Netlify publishes the folder and shows a Preview URL ending in netlify.app.

  • Record the public URL here: https://your-site.netlify.app.

Before you open the public URL, which details would prove that the deployed build matches your local production preview?

  • Open your recorded URL in a private browser window.

You should see the rotating solid rendered through both ASCII passes. The sharper shape-scored edges and visible glyph atlas confirm that the complete page is public.

That finishes the project: your reusable atlas now drives two glyph-selection strategies on a shareable page.

Does the deployed page look incomplete?

  • Confirm that you uploaded dist instead of a single file from inside it.
  • Rebuild the project after saving the latest src/main.js changes.
  • Upload the rebuilt dist folder to the site's deploy dropzone.

Help me compare my local preview with the Netlify deployment.

Secret mission

Audit Shape-Aware ASCII Edges

Turn your deployed side-by-side comparison into a visual quality test. Trace sharper edges back to specific glyph shapes to prove why shape scoring beats brightness matching.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your finished ASCII renderer available, pause local work, or delete the project. Keeping its local files and free static hosting deployment creates no ongoing project costs.

Resources you used:

  • A local Vite web project at path to your Vite project, containing index.html, src/main.js, the production build, the Three.js dependency, the glyph atlas code, and both shader passes.
  • A published static site with the shareable public URL from earlier.

Keep everything running

No cleanup is required. Choose this option while you want the public ASCII comparison ready to share.

  • Keep the static deployment published.
  • Keep the local Vite project at path to your Vite project.
  • Reuse the glyph atlas code when you want to convert another scene into ASCII.

Pause - I'll come back to this later

Pause your local work while preserving the project files. No local server is listed as running, so there is no process to stop.

  • Save any final changes in the Vite project at path to your Vite project.
  • Close your code editor if it is still open.
  • Close any browser tabs showing the local page.
  • Leave the static deployment published so the shareable URL remains available.

Delete - I don't want to use this again

Deletion is permanent. The local folder contains your reusable glyph atlas code, so this option fits when you are certain you no longer need it.

Expect the static hosting service to request confirmation before it removes the deployed site.

  • Delete the hosted page from its project settings by using the site's deletion control.
  • Complete the service's confirmation prompt.
  • Visit the shareable public URL from earlier to verify the deletion.

You should no longer see the side-by-side ASCII comparison at that URL. This confirms the hosted copy is gone.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Finder on macOS or File Explorer on Windows, then press Enter.
  • Navigate to path to your Vite project.
  • Confirm the folder contains index.html and src/main.js.
  • Move the project folder to Trash on macOS or the Recycle Bin on Windows.
  • Empty Trash or the Recycle Bin to remove the project permanently.
  • Check that path to your Vite project no longer exists.

Nice Work!

Nice Work!

You did it! Your deployed Three.js page now prints a rotating 3D solid as ASCII art. You can show the brightness pass beside the shape-scored pass to make the sharper edges visible.

You've learned how to:

  • Built an offscreen rendering pipeline for a lit rotating solid. Both fragment shaders sample the same scene texture.
  • Generated a reusable glyph atlas from a monospaced character set. Its visible preview lets you inspect every glyph used by the ASCII passes.
  • Published a production Vite build through static hosting. Your renderer now has a public URL ready to share.
  • Applied the same renderer ideas in an optional Secret Mission designed to push your skills further.

Ready to quiz yourself?