DEV Community

Cover image for Devlog: I Built a 3D Library in Three.js Without a Level Editor — So I Made My Own
Mika Flowers
Mika Flowers

Posted on

Devlog: I Built a 3D Library in Three.js Without a Level Editor — So I Made My Own

Shifting from code to a visual editor

Updated 3D Environment for Library

New Dev Setting

Reading DEV Articles

reading articles

A lot of you wonderful people enjoyed my last post on Oni, so heres an update on what I've been working on.

I've been spending the last few days building the DEV Library inside Oniria, and somewhere along the way the project stopped feeling like "a cool Three.js scene" and started feeling like I was building my own tiny game-engine workflow.

The idea behind the library is pretty simple:

DEV articles become physical books.

Instead of opening DEV.to and scrolling through cards, you walk through rooms, browse shelves, pull out an article, read it, and return to the exact place you found it.

But actually constructing that space turned out to be one of the most interesting problems I've hit so far.

Earlier versions of Oniria leaned much harder into a cyber-world aesthetic: glowing structures, highways, towering architecture, and DEV articles scattered through something closer to a futuristic city. It looked interesting, but the more I built it, the more I realized the environment wasn't helping me understand the information inside it.

Old Design

Articles were technically spatial, but the space itself didn't explain why anything was where it was. Eventually I started asking a simpler question: if thousands of articles were going to become physical objects, what kind of place would naturally organize them? The answer was obvious — a library. That shift changed the project from decorating a cyber-world with content into designing an actual information architecture: rooms became categories, shelves became collections, books became articles, and the building itself could communicate how DEV is organized.

That decision also created a completely different engineering problem. I was no longer placing a handful of futuristic landmarks; I was trying to build an entire navigable library — rooms, shelves, paths, furniture, signs, lighting, and hundreds of precise spatial relationships — almost entirely through Three.js code, without a traditional live level editor.

That progression looked like this:

cyber city
    ↓
looked cool, but lacked informational meaning
    ↓
"what spatial metaphor actually fits articles?"
    ↓
library
    ↓
rooms = information architecture
    ↓
now we need to physically build it
    ↓
wait... we don't have a level editor
    ↓
build our own tools
Enter fullscreen mode Exit fullscreen mode

The problem: I wasn't using a level editor

Before building the in-world authoring tools, most object placement happened directly in Three.js. All by code. No 3D Editor. Ha, take that, purist!

I wrote a small helper that clones one of the loaded GLB assets, then applies its world position, scale, and rotation:

const placeAsset = (
  template: THREE.Group,
  x: number,
  y: number,
  z: number,
  scale = 1,
  rotationY = 0,
  rotationX = 0,
) => {
  const instance = template.clone(true)

  instance.position.x += x
  instance.position.y += y
  instance.position.z += z

  instance.scale.multiplyScalar(scale)

  instance.rotation.y += rotationY
  instance.rotation.x += rotationX

  group.add(instance)

  return instance
}
Enter fullscreen mode Exit fullscreen mode

That made placing something fairly straightforward:

const table = placeAsset(
  readingTable,
  -16.2,   // x
  0.05,    // y
  -31.8,   // z
  1,       // scale
  Math.PI / 2,
)
Enter fullscreen mode Exit fullscreen mode

But moving it still meant editing numbers:

table.position.x += 0.8
table.position.z -= 1.2
table.rotation.y += Math.PI / 4
Enter fullscreen mode Exit fullscreen mode

Or changing the original call:

const table = placeAsset(
  readingTable,
  -15.4,
  0.05,
  -33.0,
  1,
  Math.PI * 0.75,
)
Enter fullscreen mode Exit fullscreen mode

Then:

save
↓
reload
↓
walk back across the library
↓
look at it
↓
"nope"
↓
change the numbers again
Enter fullscreen mode Exit fullscreen mode

That was basically my level editor. 😭 Below is an actual snippet of what I was working with:

old level editor

And once the environment grew beyond a handful of props, it became painfully obvious that this wasn't going to scale.

With a normal interface, I can change some CSS, refresh the browser, and immediately see whether a card needs another 16px of margin.

In a 3D world, changing:

position: [-16.2, 0, -31.8]
Enter fullscreen mode Exit fullscreen mode

might mean walking across an entire room just to discover that a table is clipping through a bookshelf.

bugs

And at this point the library wasn't just a few props.

I was placing:

  • walls and architectural pieces
  • real and decorative bookshelves
  • tables and chairs
  • rugs
  • plants
  • pavilions
  • signs
  • room entrances
  • shelf rows
  • collision boundaries
  • lights
  • eventually, empty spaces reserved for shelves that don't even exist yet

The first version of my workflow was basically:

change coordinates
      ↓
save
      ↓
reload the world
      ↓
walk to the room
      ↓
inspect the object
      ↓
realize it's wrong
      ↓
repeat
Enter fullscreen mode Exit fullscreen mode

It worked.

It was also slowly driving me insane.

The obvious answer would have been to move the whole project into a traditional level editor.

But I didn't really want another application to become the source of truth for the library.

The architecture was already deeply connected to code:

DEV data
   ↓
district
   ↓
room
   ↓
shelf slot
   ↓
Three.js placement
Enter fullscreen mode Exit fullscreen mode

Also, the Three.js editor was really freaking annoying to use.

three.js editor

At this point I realized that moving assets this way would either take forever or it would never be perfect to my liking. There had to be an easier way — and then I had an idea. Why don't we make the location of each shelf predetermined by a layout I create? Essentially a blueprint — then have Codex place the individual assets at each coord/pin that I place. So that became my next goal.

I needed to see coordinates inside the world

So instead I started building a tiny authoring system directly into Oniria.

The basic idea was simple:

If I'm standing somewhere in the world and think, "this object should go here," I should be able to record it here.

I added an in-world layout marker tool.

Now I could walk into a room, find the location visually, and drop a pin.

A marker could store something like:

{
  "label": "R1-P01",
  "roomSlot": 1,
  "districtId": "latest",
  "x": 16.154,
  "y": 0,
  "z": 5.369,
  "yaw": 1.57
}
Enter fullscreen mode Exit fullscreen mode

Suddenly I wasn't trying to mentally convert a 3D room into numbers.

I could use the room itself to produce the numbers.

The workflow became:

walk through the space
        ↓
find the location visually
        ↓
drop a marker
        ↓
capture position + rotation
        ↓
turn the marker into real architecture
Enter fullscreen mode Exit fullscreen mode

That sounds like a small change.

It completely changed how I could build the library.

using an editor

The world became my editor

For example, when I wanted to add an object to a particular room, I could physically stand where it belonged and create a marker.

Then that surveyed position could go directly back into the scene:

const surveyedPlacement = {
  x: 16.154,
  y: 0,
  z: 5.369,
  yaw: Math.PI / 2,
}

const instance = placeAsset(
  someLibraryAsset,
  surveyedPlacement.x,
  surveyedPlacement.y,
  surveyedPlacement.z,
  1,
  surveyedPlacement.yaw,
)
Enter fullscreen mode Exit fullscreen mode

I started using the same workflow for things like:

"This shelf belongs here."

"This pavilion needs to move farther from the path."

"These two signs are colliding."

"This light needs to sit over this exact area."

"This decorative bookcase should sit between these DEV shelves."
Enter fullscreen mode Exit fullscreen mode

new layout yay

Persisting the layout

I also didn't want those markers to disappear every time the browser refreshed. So the markers became Sanity documents.

A marker now has a persistent identity:

type LibraryLayoutMarker = {
  id: string
  label: string
  roomSlot: number
  districtId?: string

  x: number
  y: number
  z: number

  yaw?: number
  width?: number
  depth?: number
}
Enter fullscreen mode Exit fullscreen mode

That means a layout session can survive reloads.

I can place several markers, come back later, and continue working from the same spatial notes.

I can also export them.

For example:

{
  "format": "oniria-library-layout-pins-v1",
  "markers": [
    {
      "label": "R1-P01",
      "roomSlot": 1,
      "x": 16.154,
      "y": 0,
      "z": 5.369
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

At this point I had accidentally built something halfway between a debugging tool, a survey tool, and a very small level editor.

And it solved a real problem immediately. Yay!

What started as a frustrating layout problem ended up teaching me something bigger about the project: if Oniria is going to treat the web as a place instead of a page, then the tools for building that place matter just as much as the content inside it. The DEV Library now has its own spatial logic, persistent layout markers, searchable shelves, dynamic slots, and an architecture that can evolve without losing its sense of place. I'm still building it room by room, but that's also the part I've started to enjoy most — every time the world gets difficult to work with, I end up building a new piece of the system that makes Oniria feel a little more real.

oniria

Top comments (8)

Collapse
 
sinarezaei profile image
Sina Rezaei

The interesting part here isn't really the 3D library itself. It's the moment where the problem stops being “how do I place this object?” and becomes “why am I still editing coordinates by hand?”

The marker approach makes a lot of sense because you're using the environment itself as the authoring interface. Instead of mentally translating a visual position into x/y/z values, you stand where the object should go and let the tool capture that state.

I also like the decision not to introduce a separate editor as another source of truth. Since the library already has its own data model from DEV data → district → room → shelf, keeping the layout information inside that system avoids creating another pipeline to keep synchronized.

The Sanity persistence is a nice touch too. Once the markers have IDs and can be exported, they stop being temporary debugging helpers and become actual layout data. It feels like a good example of a useful rule in tooling: when the workflow starts fighting the project, sometimes the right fix isn't changing the workflow. It's building the missing tool.

Collapse
 
mikachu profile image
Mika Flowers

This is exactly the shift that happened while I was building it. At first I kept treating the coordinates as the problem, like i just needed to get better at estimating positions or organizing the values.

Eventually I realized the actual problem was that I was trying to author a spatial environment through numbers instead of through the environment itself. Once I could stand somewhere, drop a marker, and persist that state, the whole workflow started making much more sense.

Thanks for such a thoughtful read! 💚

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze

"The idea behind the library is pretty simple..."

...

Proceeds to build an entire game engine 😄

That's usually how it goes, though.

Your approach is really interesting. One big advantage I see is that the engine becomes reusable, so you can build other cool stuff on top of it.

And speaking of other cool stuff: I love the cyber city idea. It reminds me of Cyberpunk and Night City, which I'm a huge fan of, so I'd definitely love to see that too. Imagine reading DEV articles in City Center or the Badlands. Mmm... what an experience.

Okay, back to Earth: I can't wait to try out the library.

Collapse
 
plastikelectrik profile image
Plastik Electrik

Great job, waiting to see it working 😃

Collapse
 
mikachu profile image
Mika Flowers

Thank you! It's coming along nicely, designing the 3D layout has been my favorite part so far :)

Collapse
 
plastikelectrik profile image
Plastik Electrik

i understand you 100% enjoy it!!!

Collapse
 
dylnd0g profile image
Giacomo

Making the markers Sanity documents with stable IDs is the smart move here, it turns debug state into real layout data. Snapping yaw and position to a grid plus a bounding-box check on drop would catch most clipping before the walkthrough. Are the pins resolved from Sanity at runtime or baked in at build time?

Collapse
 
pavel_kkkkazantsev profile image
Pavel Kazantsev

Great job! Good luck!