Library / SDK
Kenton-GMI/sakura-crossing avatar
Kenton-GMI/sakura-crossing

Sakura Crossing: a Three.js railway town drawn with Canvas2D, not textures

An explorable Japanese suburban railway-crossing neighbourhood on a small planet, rendered 3D-to-2D as a cel-shaded anime background. Three.js, no image assets.

541 stars103 forksJavaScriptMIT

At a glance

What is it?
Sakura Crossing is an explorable Japanese suburban neighbourhood built as real 3D geometry and rendered to look like a cel-shaded anime background. It ships with one dependency beyond Vite, no image assets in src/, and a clear set of trade-offs for anyone thinking of using it as a starting point.
Who is it for?
Sakura Crossing is worth adopting if you want a working, MIT-licensed reference for toon shading, screen-space ink and runtime Canvas2D signage in Three.js, and you are comfortable reading the source rather than following a tutorial. It is the wrong choice if you need a maintained library with releases, an API surface, or documentation beyond the README, because the last push was on 2026-07-29 and no releases were retrieved.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 68 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Sakura Crossing actually is, and who it is for

Sakura Crossing is a single explorable place, not a game engine or a component library. The README describes it as "an explorable Japanese suburban neighbourhood in blossom season, built as a real 3D scene in Three.js and rendered to look like a hand-painted 2D animation background." It starts at a level crossing and grows outward: a shopping street, a shrine up a flight of stone steps, a high school, a pedestrian overbridge, a branch library, two back alleys, a festival ground caught mid-preparation, and a community-bus turnaround where the estate meets a hillside. A railway runs the whole way round the planet and through the hills twice.

The intended audience is narrow and worth stating plainly. This is for graphics programmers and technical artists who want to read working code for a specific rendering problem, and for people who want a small, self-contained world to walk around in. It is not for someone who wants to drop a character controller into an existing project, because there is no packaged API to import. There are no people in the scene at all, and the README says that is deliberate: "everything the place has to say about itself is said by what has been left out, pinned up, parked or forgotten." That is an artistic position, not a missing feature, but it does mean you will not find animation rigs or NPC logic to borrow.

How the cel-shaded look is built from real geometry

The README is explicit that nothing is a 2D image placed in a 3D world. Every object is real geometry, and the drawn look comes from four layers on top of it.

The first is quantised light with a tinted shadow, in src/core/toon.js. Everything uses MeshToonMaterial with a hand-authored gradient ramp, so direct sun is stepped into two to four flat bands. The toon BRDF is then patched through lights_toon_pars_fragment so the darker bands shift in hue toward a cool violet instead of simply darkening. The README argues that this hue shift "is most of what separates 'anime cel' from 'low-poly 3D'." Pale masses that need to stay light on their shadow side, such as blossom and drinks behind glass, use a deliberately high-key ramp selected with bands: 'soft'.

Blossom gets a further exception: the canopies do not receive shadow at all. The reasoning in the README is concrete. A ramp only shapes direct light, so once the shadow map zeroes the sun, a canopy falls back to ambient and reads as a dark violet lump. Because a large cherry self-shadows heavily, whole trees were going grey with isolated dark blobs against the sky. Turning receive off makes the canopy behave the way blossom is painted: a flat high-key mass whose form comes from its three tones, lit the same wherever it stands. It still casts shadow, which is what dapples the ground beneath it.

The second layer is screen-space ink in src/core/post.js. Lines come from the second difference of linearised depth, read from a depth texture attached to the scene render target. The README gives the reason for the second difference over the first: a first difference would smear ink across the road wherever the surface grazes the camera, while a second difference stays flat across any planar surface no matter how oblique it is. That is a real constraint on the technique, and it is the part most likely to behave differently on other hardware, because it depends on the depth buffer's precision and your camera's near plane.

Installing it and walking to the shrine

The project needs Node 18 or newer and nothing else. Three.js and Vite are the only two dependencies, and package.json confirms that: three is the sole runtime dependency and vite is the sole dev dependency. Clone the repository, then install and start the production bundle:

bash
npm install
npm run play     # build + serve the production bundle, port 5179

npm run play runs vite build and then vite preview on port 5179 with the browser opened for you. If you are working on the code instead, npm run dev starts the dev server with HMR on port 5178. Open the printed URL and click to explore. Keep the terminal open, because closing it stops the server. The README warns that a plain file:// open will not work, since the project is ES modules and needs to be served over HTTP.

The controls are keyboard and mouse. W A S D walks, Shift runs, the mouse looks, and E interacts. V calls the e-bike, P orbits out to look at the whole planet, C shows coordinates, R returns to the opening view, and O and G toggle the ink pass and the colour grade so you can see what each one contributes.

A first real use is to walk the route the README lays out. From the crossing, go south-east through the alley beside the corner shop to reach the shopping street, then north past two Showa shopfronts to the north lane. Turn left for the library and the phone box, right for the residential lane. The shrine is west along the near-side lineside path and up a two-metre alley between two houses, with the festival ground immediately west of its forecourt. Pressing C at any point gives you a ready-made camera line, and Shift+C copies it to the clipboard, which is the fastest way to reproduce a view you want to study.

If you want your own music, the README says to drop files into public/audio/ and list the filenames in TRACKS at the top of src/core/audio.js. Only one track ships with the repository, because the rest of the playlist it was developed against is commercial music that cannot be redistributed. Everything in that folder is git-ignored except the shipped track. An empty playlist is a supported state.

The interactables, the e-bike and the train timetable

Twenty-two things answer the E key. Nineteen vending machines dispense a can, two of them belonging to スーパー さかえ. The shop shutter rolls up and down, the cat stretches, and the relay box on the far corner calls a train. Otherwise a train comes through on its own every 35 to 45 seconds, with bells and lamps first, then the booms, then the train. That cycle is the scene's clock and the reason the crossing reads as a place rather than a backdrop.

The 電動バイク is the more interesting piece of engineering. V stands one in front of you, a 原付, the same machine twelve of which are parked around the town and painted a warm coral so it can be found at forty metres. Looking at it and pressing E gets you on. Riding keeps the same first-person camera, dropped onto the seat, with the speedometer, mirrors and bar-ends in frame. W opens the throttle to 7.65 m/s, which the README describes as one and a half times a run. S brakes and then reverses at a walk. A and D steer rather than strafe, and the mouse still steers too, with the machine banking into turns against the horizon.

E gets you off beside it and it stays where you left it, on its stand, in the way. V calls it back if it is more than four metres off, and puts it away if it is not. The README calls it the only thing in this world with wheels that moves other than the train, and the only reasonable way to get from the crossing to ひばり山's 展望台 and back. Notice what this design does not include: no vehicle physics, no collision damage, no traffic. It is a movement affordance sized to the map, and the four-metre rule for recalling it is the whole inventory system.

Where the project stops being the right tool

The most honest limitation is the one the README states about itself: there is no documentation beyond it. There is no API reference, no tutorial for adding a building, and no plugin surface. If you want to extend the town, you are reading src/ and following the existing patterns. That is a reasonable ask for a graphics programmer and a poor fit for anyone hoping to configure the scene from a data file.

The second limitation is the depth-based ink pass. Because lines come from the second difference of linearised depth, the effect depends on the scene's depth texture and your camera's near and far planes. The README explains why the second difference was chosen, but it does not document how the technique behaves across GPUs or precision settings. If you port src/core/post.js into a scene with a much larger draw distance or a different depth format, expect to retune it. This is a technique demonstrated in one scene, not a general-purpose outline shader.

The third is the missing people. The absence is deliberate and central to the atmosphere, but it means the repository has nothing to teach about character animation, pathing or crowds. If your project needs those, this is the wrong reference to start from, regardless of how close the visual style is to what you want.

Finally, the licensing of the audio is a practical constraint. Only one track ships because the rest is commercial music that cannot be redistributed. The code is MIT, but the music is not, and the README's own solution is to point TRACKS at files you supply yourself.

How it compares to an outlined NPR pipeline

The obvious alternative approach is a conventional non-photorealistic pipeline built on inverted-hull outlines plus a posterise post-process, the pattern common in Three.js examples and in engines like Unity's URP with a toon shader. The difference is where the line comes from. An inverted hull draws the outline as a second pass over the geometry, scaled along its normals, so it produces a line around silhouette edges but not around interior creases unless you add more passes. Sakura Crossing instead derives every line from depth in screen space, which means the same pass can fire on interior creases and on silhouettes without per-object setup.

The trade-off runs the other way too. An inverted hull is robust to depth precision and works with any camera range, because it never samples the depth buffer. The second-difference approach is sensitive to exactly the things an inverted hull ignores. There is also the question of assets: a conventional pipeline usually assumes textures and normal maps, while Sakura Crossing's README notes that every sign, fascia, lantern and price strip is drawn at runtime with Canvas2D and that there is not one image asset in src/. That makes the repository unusually portable, since there are no binary assets to ship or license, but it also means the visual detail lives in drawing code rather than in an art pipeline your team may already have.

Editorial conclusion

Sakura Crossing is worth adopting if you want a working, MIT-licensed reference for toon shading, screen-space ink and runtime Canvas2D signage in Three.js, and you are comfortable reading the source rather than following a tutorial. It is the wrong choice if you need a maintained library with releases, an API surface, or documentation beyond the README, because the last push was on 2026-07-29 and no releases were retrieved. Before you build on it, check the second-difference ink pass in src/core/post.js against your own depth precision and camera near plane, since that is where the look is most likely to break on different hardware.

Frequently asked questions

How do I install and run Sakura Crossing?

Install Node 18 or newer, run npm install, then npm run play to build and serve the production bundle on port 5179. Use npm run dev instead for the HMR dev server on port 5178. Opening the files directly over file:// will not work because the project is ES modules and needs to be served over HTTP.

Does Sakura Crossing use any image textures?

No. The README states there is not one image asset in src/, and that every sign, fascia, lantern and price strip is drawn at runtime with Canvas2D. The cel-shaded look comes from MeshToonMaterial with a hand-authored gradient ramp, a hue-shifted toon BRDF, screen-space ink from the second difference of depth, and a colour grade.

How do I add my own music to Sakura Crossing?

Drop audio files into public/audio/ and list their filenames in the TRACKS array at the top of src/core/audio.js. Only one track ships with the repository, and everything else in that folder is git-ignored. An empty playlist is a supported state, in which case the world runs in silence.

What are the controls for the e-bike in Sakura Crossing?

Press V to stand an e-bike in front of you, look at it and press E to get on. W opens the throttle to 7.65 m/s, S brakes and then reverses at a walk, and A and D steer rather than strafe. Press E again to get off, or V to call it back if it is more than four metres away and to put it away if it is not.

Official sources

  1. Issues
  2. Kenton-GMI/sakura-crossing on GitHub
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kenton-gmi-sakura-crossing.svg)](https://hysenlabs.com/projects/kenton-gmi-sakura-crossing)