Endless backrooms in Babylon.js!

I made backrooms on the babylon engine, move using the arrows ←, →, ↑, ↓ on the keyboard and mouse.

PC version

Original backrooms

In the zero version, I added an infinite procedurally generated carpet.

Backrooms in Babylon.js (Version 0) | Babylon.js Playground

In the first version, I added randomly generated and randomly rotated walls to this procedurally generated carpet.

Backrooms in Babylon.js (Version 1) | Babylon.js Playground

In the second version, I added an infinite procedurally generated ceiling.

Backrooms in Babylon.js (Version 2) | Babylon.js Playground

In the third version, I added infinitely procedurally generated evenly spaced lamps.

Backrooms in Babylon.js (Version 3) | Babylon.js Playground

In the fourth version, I did as in the original backroom, which talks about millions of square miles of randomly combined empty rooms, which means that the lamps in it are also in random places and not located in it periodically, I also added a teleporter by coordinates, and a display board for camera coordinates.

Backrooms in Babylon.js (Version 4) | Babylon.js Playground

Well, we’re missing something again, because backrooms is an endless maze, not just a set of random walls in an endless space. Let’s make an endless maze! And besides, it doesn’t have one solid wallpaper, but a set of different wallpapers.

Backrooms in Babylon.js (Version 5) | Babylon.js Playground

Kane Pixel’s version

In the first version, I added the ability to add structure generation based on the faces and vertex arrays. You just need to fly a little to the side to see the generation.

Kane Pixel’s Backrooms in Babylon.js (Version 0) | Babylon.js Playground

In the second version, I added different biomes, and now not one biome is generated, but many.

Kane Pixel’s Backrooms in Babylon.js (Version 1 - multybiom generation) | Babylon.js Playground

But something is still missing, because Kane Pixel’s backrooms are an endless layered multi-storey pie of randomly procedurally generated, interconnected liminal spaces.

Kane Pixel’s Backrooms in Babylon.js (Version 2 - Multistory multibiome generation) | Babylon.js Playground

But still, something is wrong again, because the textures in the backroom model from Kane Pixel are different and not monotonous, let’s add the ability to make textures different, and plus I added the ability to teleport.

Kane Pixel’s Backrooms in Babylon.js (Version 3 - Different textures) | Babylon.js Playground

Backrooms editor

So now we’ve almost created a full-version backrooms, the only thing left to do is just a couple of things, one of which is to create a backrooms editor, let’s do it, and that’s what I’ve done well.

I added a lot of mirrored structures, and a lot of gizmos tied only to a lot of the original cubes, but the chunks were generated only in XZ coordinates…

Backrooms editor XZ version | Babylon.js Playground

There is a version with XYZ coordinates.

Backrooms editor XZY version

You can view all versions of this editor in my separate topic → Mirrored gizmos.

Mobile version

Original mobile backrooms

But while browsing my backrooms from my smartphone, I found a problem that I can’t navigate on my phone, and therefore in the mobile version of backrooms I added arrows with which I can navigate.

Backrooms on Babylon.js (Mobile version 3) | Babylon.js Playground

In the fourth version, I removed the arrows and added a joystick to make the player move more smoothly.

Backrooms on Babylon.js (Mobile version 4) | Babylon.js Playground

Kane Pixel’s mobile version

But I can’t move on my mobile phone like I can on my computer, I need to add the displacement arrows.

Kane Pixel’s Backrooms on Babylon.js (Mobile version 2) | Babylon.js Playground

But the movement arrows don’t move smoothly, let’s add a joystick.

Kane Pixel’s Backrooms on Babylon.js (Mobile version 3) | Babylon.js Playground

Optional versions

The optional versions are a kind of sandbox for the realization of a variety of fantasies and experiments.

Backrooms in Babylon.js (Optional version - The infernal Valley of pillars) | Babylon.js Playground

Imagine an endless backroom without walls, like in that video I made this version.

Backrooms in Babylon.js (Optional version - no walls) | Babylon.js Playground

Now I’ve added a new optional version in which biomes are generated by cubes of biomes in multistory, and you need to wait a bit when launching.

Kane Pixel’s Backrooms in Babylon.js (Optional version - Cubic multi-storey biome generation) | Babylon.js Playground

There is also a version of backrooms with multi-storey cake generation of biomes, that is, on each floor, make the same biomes in order, that is, on the first floor, the biome is the first in order in the dictionary of biomes, on the second floor, the second biome, and so on…

Kane Pixel’s Backrooms in Babylon.js (Optional version - Cake multi-storey biome generation) | Babylon.js Playground

P.S.

Now it’s a full-fledged backroom space! Maybe someone will come up with some new projects and the project will develop.

12 Likes

Have a try with Instances as a replacement of MeshBuilder, create once, add instances then。

The point is not that, but that the structures are generated in each other and do not merge when they intersect! There was an idea to rewrite the union function python on babylon, but nothing good came of it…

function createChunk(position, rotation, scene) {
    const vertices = [
        [-6.0, -2.0, -6.0], [6.0, -2.0, -6.0], [6.0, -2.0, -0.75],
        [-6.0, -2.0, 6.0], [6.0, -2.0, 0.75], [6.0, -2.0, 6.0],
        [-6.0, 2.0, -6.0], [6.0, 0.0, -0.75], [6.0, 2.0, -6.0],
        [-6.0, 2.0, 6.0], [6.0, 0.0, 0.75], [6.0, 2.0, 6.0],
        [25.0, -2.0, -0.75], [25.0, -2.0, 0.75], [25.0, 0.0, -0.75],
        [25.0, 0.0, 0.75]
    ];

    const faces = [
        [0, 1, 3], [1, 0, 6], [2, 3, 1],
        [2, 1, 8], [3, 9, 0], [4, 3, 2],
        [4, 10, 5], [3, 4, 5], [11, 3, 5],
        [6, 0, 9], [1, 6, 8], [7, 2, 8],
        [7, 8, 10], [8, 6, 11], [9, 3, 11],
        [9, 11, 6], [11, 5, 10], [8, 11, 10],
        [2, 12, 4], [7, 14, 2], [12, 2, 14],
        [12, 14, 13], [4, 13, 10], [4, 12, 13],
        [15, 10, 13], [15, 13, 14], [7, 15, 14],
        [10, 15, 7]
    ];

    return createMeshFromArrays(vertices, faces, scene, position, rotation);
}

function createMeshFromArrays(vertices, faces, scene, position, rotation) {
    const positions = [];
    const indices = [];
    const uvs = [];

    vertices.forEach(vertex => {
        positions.push(vertex[0], vertex[1], vertex[2]);

        // Add UV coordinates for each vertex
        uvs.push(vertex[0] / 12 + 0.5, vertex[2] / 12 + 0.5);
    });

    faces.forEach(face => {
        indices.push(face[0], face[1], face[2]);
        indices.push(face[2], face[1], face[0]);
    });

    const mesh = new BABYLON.Mesh("customMesh", scene);
    const vertexData = new BABYLON.VertexData();

    vertexData.positions = positions;
    vertexData.indices = indices;
    vertexData.uvs = uvs;

    vertexData.applyToMesh(mesh);

    const material = new BABYLON.StandardMaterial("material", scene);
    material.diffuseTexture = new BABYLON.Texture("https://i.postimg.cc/Pq7xgZY0/wallpaper.jpg", scene);
    material.backFaceCulling = false;

    mesh.material = material;

    // Устанавливаем положение и поворот
    mesh.position = new BABYLON.Vector3(position[0], position[1], position[2]);
    mesh.rotation.y = rotation; // Поворот вокруг оси Y

    return mesh;
}

function unionMeshes(mesh1, mesh2) {
    const positions1 = [];
    const indices1 = [];
    const uvs1 = [];

    mesh1.vertexData.positions.forEach((position, index) => {
        positions1.push(position);
        indices1.push(index);
    });

    mesh1.vertexData.uvs.forEach((uv, index) => {
        uvs1.push(uv);
    });

    const positions2 = [];
    const indices2 = [];
    const uvs2 = [];

    mesh2.vertexData.positions.forEach((position, index) => {
        positions2.push(position);
    });

    mesh2.vertexData.indices.forEach((index) => {
        indices2.push(index + mesh1.vertexData.positions.length / 3);
    });

    mesh2.vertexData.uvs.forEach((uv, index) => {
        uvs2.push(uv);
    });

    const combinedPositions = positions1.concat(positions2);
    const combinedIndices = indices1.concat(indices2);
    const combinedUvs = uvs1.concat(uvs2);

    const combinedMesh = new BABYLON.Mesh("combinedMesh", scene);
    const vertexData = new BABYLON.VertexData();

    vertexData.positions = combinedPositions;
    vertexData.indices = combinedIndices;
    vertexData.uvs = combinedUvs;

    vertexData.applyToMesh(combinedMesh);

    return combinedMesh;
}

var createScene = function () {
    var scene = new BABYLON.Scene(engine);
    var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
    camera.setTarget(BABYLON.Vector3.Zero());
    camera.attachControl(canvas, true);
    var light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
    light.intensity = 0.7;

    const numChunks = 10;
    let combinedMesh = null;

    for (let i = 0; i < numChunks; i++) {
        const position = [Math.random() * 30 - 15, 1, Math.random() * 30 - 15];
        const rotation = Math.random() * Math.PI * 2; // Случайный угол поворота

        const mesh = createChunk(position, rotation, scene);

        if (combinedMesh === null) {
            combinedMesh = mesh;
        } else {
            combinedMesh = unionMeshes(combinedMesh, mesh);
        }
    }

    return scene;
};

Do you have any ideas?

merging solid meshes? have a try with CSG2

It doesn’t work.

It doesn’t work | Babylon.js Playground

It doesn’t work. Example 2. | Babylon.js Playground

It doesn’t work. Example 3. | Babylon.js Playground

I also have no idea how to make different textures for different faces, I made several unsuccessful attempts, and if anyone has any ideas, please suggest them.

let existingChunks = {};
const CHUNK_SIZE = 100; // Размер чанка
const MAX_HEIGHT = 2; // Максимальная высота для генерации чанков

// Измените этот массив на свои биомы
const biomes = [
    {
        vertices: [[-11.140454, -1.0, 6.045034], [-5.874499, -1.000002, -25.147928], ...],
        verticalTexture: "https://i.postimg.cc/Pq7xgZY0/wallpaper.jpg",
        topHorizontalTexture: "https://i.postimg.cc/y83ChCs2/image.png",
        bottomHorizontalTexture: "https://i.postimg.cc/wvStLGvz/image.jpg"
    },
    {    
        vertices: [[-7.000378, 0.999999, -4.244266], [-6.353668, 0.999999, -5.322138], ...],
        faces: [[0, 2, 25], [25, 2, 1], ...],
        verticalTexture: "https://i.postimg.cc/Pq7xgZY0/wallpaper.jpg",
        topHorizontalTexture: "https://i.postimg.cc/y83ChCs2/image.png",
        bottomHorizontalTexture: "https://i.postimg.cc/wvStLGvz/image.jpg"
    }
];
// Функция для создания коридора
function createCorridor(chunkId, x, y, z, scene) {
    const biome = biomes[Math.floor(Math.random() * biomes.length)]; // Случайный выбор биома
    return createMeshFromArrays(biome.vertices, biome.faces, scene, chunkId, x, y, z, biome);
}

// Функция для создания сцены в Babylon.js
async function createScene() {
    const engine = new BABYLON.Engine(canvas, true);
    const scene = new BABYLON.Scene(engine);

    const camera = new BABYLON.FreeCamera("FreeCam", new BABYLON.Vector3(0, 10, -10), scene);
    camera.setTarget(BABYLON.Vector3.Zero());
    camera.attachControl(canvas, true);

    new BABYLON.HemisphericLight("Hemispheric Light", new BABYLON.Vector3(1, 1, 0), scene).intensity = 0.7;

    scene.registerBeforeRender(() => {
        GenerateChunks(camera.position, scene);
    });

    engine.runRenderLoop(() => {
        scene.render();
    });

    return scene;
}

// Функция для генерации чанков коридоров
function GenerateChunks(position, scene) {
    const chunkX = Math.floor(position.x / CHUNK_SIZE);
    const chunkZ = Math.floor(position.z / CHUNK_SIZE);
    const chunkY = Math.floor(position.y / CHUNK_SIZE); // Новая ось Y

    for (let x = chunkX - 2; x <= chunkX + 2; x++) {
        for (let z = chunkZ - 2; z <= chunkZ + 2; z++) {
            for (let y = chunkY - MAX_HEIGHT; y <= chunkY + MAX_HEIGHT; y++) { // Генерация по Y
                const chunkId = `${x},${y},${z}`; // Обновленный идентификатор чанка
                if (!existingChunks[chunkId]) {
                    createCorridor(chunkId, x, y * CHUNK_SIZE, z, scene); // Обновленный вызов
                    existingChunks[chunkId] = true; // Сохраняем существующий чанк
                }
            }
        }
    }
}

// Обновленная функция createMeshFromArrays для работы с коридорами
function createMeshFromArrays(vertices, faces, scene, chunkId, x, y, z, biome) {
    const positions = [];
    const indices = [];
    const uvs = [];

    vertices.forEach(vertex => {
        positions.push(vertex[0] + x * CHUNK_SIZE, vertex[1] + y, vertex[2] + z * CHUNK_SIZE); // Учитываем ось Y
    });

    let faceCount = 0;

    faces.forEach(face => {
        const startIndex = faceCount * 3; // Каждая грань содержит 3 индекса
        indices.push(face[0], face[1], face[2]); // Индексы вершин
        // Обратный порядок
        indices.push(face[2], face[1], face[0]);
        faceCount++;
    });

    const mesh = new BABYLON.Mesh(chunkId, scene);
    const vertexData = new BABYLON.VertexData();

    vertexData.positions = positions;
    vertexData.indices = indices;

    // Установка UV-кординат
    const uv = [0, 0, 1, 0, 1, 1, 0, 1];
    vertexData.uvs = new Array(indices.length).fill(0).map((_, i) => uv[i % 4]); // Повторяем UV для каждой грани
    vertexData.applyToMesh(mesh);

    // Создание материалов
    const verticalMaterial = new BABYLON.StandardMaterial(`verticalMaterial_${chunkId}`, scene);
    verticalMaterial.diffuseTexture = new BABYLON.Texture(biome.verticalTexture, scene);

    const topMaterial = new BABYLON.StandardMaterial(`topMaterial_${chunkId}`, scene);
    topMaterial.diffuseTexture = new BABYLON.Texture(biome.topHorizontalTexture, scene);

    const bottomMaterial = new BABYLON.StandardMaterial(`bottomMaterial_${chunkId}`, scene);
    bottomMaterial.diffuseTexture = new BABYLON.Texture(biome.bottomHorizontalTexture, scene);

    // Создаем подмеши для каждой грани
    let subMeshCount = faces.length; // Количество граней
    for (let i = 0; i < subMeshCount; i++) {
        const startIndex = i * 6; // Каждый подмеш содержит 2 треугольника (6 индексов)
        const endIndex = startIndex + 6;

        const subMesh = new BABYLON.SubMesh(i, 0, positions.length / 3, startIndex, endIndex, mesh);
        
        // Применяем правильный материал к каждой грани
        if (i === 0) { // Условие для верхней грани
            subMesh.materialIndex = topMaterial.id;
        } else if (i === 1) { // Условие для нижней грани
            subMesh.materialIndex = bottomMaterial.id;
        } else {
            subMesh.materialIndex = verticalMaterial.id; // Для остальных вертикальных граней
        }
    }

    return mesh;
}

// Запуск инициализации сцены
createScene();

Example 2

const CHUNK_SIZE = 100; // Размер чанка
const MAX_HEIGHT = 2; // Максимальная высота для генерации чанков

// Измените этот массив на свои биомы
const biomes = [
    {
        vertices: [[-11.140454, -1.0, 6.045034], [-5.874499, -1.000002, -25.147928], ...],
        faces: [[10, 0, 35], [1, 3, 2], ...],
        verticalTexture: "https://i.postimg.cc/Pq7xgZY0/wallpaper.jpg",
        topHorizontalTexture: "https://i.postimg.cc/y83ChCs2/image.png",
        bottomHorizontalTexture: "https://i.postimg.cc/wvStLGvz/image.jpg"
    },
    {
        vertices: [[-7.000378, 0.999999, -4.244266], [-6.353668, 0.999999, -5.322138], ...],
        faces: [[0, 2, 25], [25, 2, 1], ...],
        verticalTexture: "https://i.postimg.cc/Pq7xgZY0/wallpaper.jpg",
        topHorizontalTexture: "https://i.postimg.cc/y83ChCs2/image.png",
        bottomHorizontalTexture: "https://i.postimg.cc/wvStLGvz/image.jpg"
    }
];

// Функция для создания коридора
function createCorridor(chunkId, x, y, z, scene) {
    const biome = biomes[Math.floor(Math.random() * biomes.length)]; // Случайный выбор биома
    return createMeshFromArrays(biome.vertices, biome.faces, scene, chunkId, x, y, z, biome);
}

// Функция для создания сцены в Babylon.js
async function createScene() {
    const engine = new BABYLON.Engine(canvas, true);
    const scene = new BABYLON.Scene(engine);

    const camera = new BABYLON.FreeCamera("FreeCam", new BABYLON.Vector3(0, 10, -10), scene);
    camera.setTarget(BABYLON.Vector3.Zero());
    camera.attachControl(canvas, true);

    new BABYLON.HemisphericLight("Hemispheric Light", new BABYLON.Vector3(1, 1, 0), scene).intensity = 0.7;

    scene.registerBeforeRender(() => {
        GenerateChunks(camera.position, scene);
    });

    engine.runRenderLoop(() => {
        scene.render();
    });

    return scene;
}

// Функция для генерации чанков коридоров
function GenerateChunks(position, scene) {
    const chunkX = Math.floor(position.x / CHUNK_SIZE);
    const chunkZ = Math.floor(position.z / CHUNK_SIZE);
    const chunkY = Math.floor(position.y / CHUNK_SIZE); // Новая ось Y

    for (let x = chunkX - 2; x <= chunkX + 2; x++) {
        for (let z = chunkZ - 2; z <= chunkZ + 2; z++) {
            for (let y = chunkY - MAX_HEIGHT; y <= chunkY + MAX_HEIGHT; y++) { // Генерация по Y
                const chunkId = `${x},${y},${z}`; // Обновленный идентификатор чанка
                if (!existingChunks[chunkId]) {
                    createCorridor(chunkId, x, y * CHUNK_SIZE, z, scene); // Обновленный вызов
                    existingChunks[chunkId] = true; // Сохраняем существующий чанк
                }
            }
        }
    }
}

// Обновленная функция createMeshFromArrays для работы с коридорами
function createMeshFromArrays(vertices, faces, scene, chunkId, x, y, z, biome) {
    const positions = [];
    const indices = [];
    const uvs = [];

    vertices.forEach(vertex => {
        positions.push(vertex[0] + x * CHUNK_SIZE, vertex[1] + y, vertex[2] + z * CHUNK_SIZE); // Учитываем ось Y
    });

    faces.forEach(face => {
        indices.push(face[0], face[1], face[2]); // Индексы вершин
        indices.push(face[2], face[1], face[0]); // Обратный порядок
    });

    // Установка UV-кординат
    const uv = [0, 0, 1, 0, 1, 1, 0, 1];
    uvs.push(...uv);

    const mesh = new BABYLON.Mesh(chunkId, scene);
    const vertexData = new BABYLON.VertexData();

    vertexData.positions = positions;
    vertexData.indices = indices;
    vertexData.uvs = uvs;

    vertexData.applyToMesh(mesh);

    const material = new BABYLON.StandardMaterial(`material_${chunkId}`, scene);

    // Установка текстур в зависимости от расположения
    if (y > 0) { // Для верхних горизонтальных границ
        material.diffuseTexture = new BABYLON.Texture(biome.topHorizontalTexture, scene);
    } else if (y < 0) { // Для нижних горизонтальных границ
        material.diffuseTexture = new BABYLON.Texture(biome.bottomHorizontalTexture, scene);
    } else { // Для вертикальных границ
        material.diffuseTexture = new BABYLON.Texture(biome.verticalTexture, scene);
    }

    material.backFaceCulling = false;
    mesh.material = material;

    return mesh;
}

// Запуск инициализации сцены
createScene();

But I’ll come back later and figure something out.

But what I found interesting for myself in the 3rd version of the Kane Pixel version is that the further we move away from the initial coordinates, the more buggy the world becomes - like a kind of distant lands like in Minecraft only in backrooms.
Запись_2025_03_09_11_54_50_319
Why do you think this is happening?

On the arrows in the mobile version, it is better to use held elements instead of clicking.

function setOnTouch(elem,start,end){
elem.addEventListener("touchstart",(e)=>{
    if(start!=null){ try{ start(); }
   catch(err){ alert(err); }}
    e.preventDefault();
});
elem.addEventListener("touchend",(e)=>{
    if(end!=null){ try{ end(); }
    catch(err){ alert(err); }}
    e.preventDefault();
});
}


setOnTouch( yourButtonUp, ()=>{
dirX=1;
},()=>{
dirX=0;
});
// another buttons

var dirX=0, dirZ=0;

setInterval(()=>{
camera.position.x+=dirX;
camera.position.z+=dirZ;
},1000/60);



Also, the arrow images have not transparent background.

It is clear that this is not important, but it is easy to make improvements.

Well, what’s even more interesting is that if you teleport far enough away in my 4 version of the original backrooms, the distant lands will be different from the Kane Pixel version of the backrooms version in which the distant lands looked like grainy black textures, and in the original it’s a very strange glitch that also differs the further we go He’s getting all the weirder.

for example, when teleporting to the coordinates X: 11111111, Y: 1, Z: 11111111, the backroom becomes loose, the walls start to twitch nervously from here to here.
Запись_2025_03_10_17_45_27_814
And if you teleport even further, it becomes even more strange, I do not even know how to describe it…
Запись_2025_03_10_18_08_32_246
And if you teleport even further, almost to the very edge, then it turns into some kind of completely broken world consisting of a porridge wall glitches.
Запись_2025_03_10_18_19_39_102

Great, I solved the problem with CSG2! I just can’t figure out why the ceiling is broken? :sweat_smile:

solved the problem with CSG2 for OBJ structures | Babylon.js Playground

It should get like in my python code → The most accurate version.

Now you can make an infinite version!

solved the problem with CSG2 for OBJ structures (endless version) | Babylon.js Playground

I would like to ask if anyone is reading this topic!? If those who are really interested in it?

I took note of your comment and made a joystick for moving for the new mobile version of backrooms.

Kane Pixel’s Backrooms on Babylon.js (Mobile version 3) | Babylon.js Playground

Only you need to teleport to the z coordinates: 260+

There is also a fourth version of the original backrooms.

Backrooms on Babylon.js (Mobile version 4) | Babylon.js Playground