How to make a ultra-optimized voxel terrain like this in a HTML file?

This is the terrain:

And my objective is to optimize it. Its possible? Thanks anyway!

May… this???:

Im thinking not.

Where is your source image from?

There are a few well known techniques for doing large voxel fields.
greedy meshing..
sparse voxel octtrees..
raymarched texture encoded distance fields..
They are all pretty complex to implement.

You’ll also need to use texture atlasses or texture arrays to get textured cubes.

marched volumes:

greedy meshing:

A case against greedy meshing:

SVO:

It’s a HUUUUUGE topic.

4 Likes

I did this for the moment:

<script>
onload=()=>{
  k=[]
  xa=0
  ya=0
  onkeydown=onkeyup=(e)=>{k[e.keyCode]=e.type=="keydown"}
  document.body.requestPointerLock=document.body.requestPointerLock||document.body.mozRequestPointerLock
  document.title="Voxel Sphere."
  document.body.style.margin=0
  scene=new THREE.Scene()
  camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,0.1,10000)
  renderer=new THREE.WebGLRenderer()
  document.body.appendChild(renderer.domElement)

  // Dimensiones
  const size = 100;
  const blockSize = 1;
  const blocks = new Set();

  // Simula todos los bloques llenos
  for (let x = 0; x < size; x++) {
    for (let y = 0; y < size; y++) {
      for (let z = 0; z < size; z++) {
        if(Math.random()<0.9){
          blocks.add(`${x},${y},${z}`);
        }
      }
    }
  }

  // Caras unitarias (de 1x1) en cada eje positivo
  const faceOffsets = [
    { dir: [1, 0, 0], verts: [[0.5, -0.5, -0.5], [0.5, 0.5, -0.5], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5]] },  // Right
    { dir: [-1, 0, 0], verts: [[-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, -0.5]] }, // Left
    { dir: [0, 1, 0], verts: [[-0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, -0.5], [-0.5, 0.5, -0.5]] },  // Top
    { dir: [0, -1, 0], verts: [[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5]] }, // Bottom
    { dir: [0, 0, 1], verts: [[-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], [0.5, 0.5, 0.5], [-0.5, 0.5, 0.5]] },   // Front
    { dir: [0, 0, -1], verts: [[0.5, -0.5, -0.5], [-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5]] } // Back
  ];

  const positions = [];
  const indices = [];

  let index = 0;

  for (let x = 0; x < size; x++) {
    for (let y = 0; y < size; y++) {
      for (let z = 0; z < size; z++) {
        const pos = `${x},${y},${z}`;
        if (!blocks.has(pos)) continue;

        for (const { dir, verts } of faceOffsets) {
          const [dx, dy, dz] = dir;
          const neighbor = `${x + dx},${y + dy},${z + dz}`;
          if (!blocks.has(neighbor)) {
            // Esta cara es visible, agregarla
            const face = verts.map(([vx, vy, vz]) => [
              vx + x,
              vy + y,
              vz + z
            ]);

            const baseIndex = index;
            face.forEach(v => positions.push(...v));
            indices.push(
              baseIndex, baseIndex + 1, baseIndex + 2,
              baseIndex, baseIndex + 2, baseIndex + 3
            );
            index += 4;
          }
        }
      }
    }
  }

  // Crear geometrĂ­a y malla
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();

  const material = new THREE.MeshStandardMaterial({ map : new THREE.TextureLoader().load("dirt.jpg") });
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);

  scene.background=new THREE.Color("rgb(0,127,255)")
  scene.fog=new THREE.Fog("rgb(0,127,255)",100,500)
  light=new THREE.PointLight("rgb(255,255,255)",1,500)
  scene.add(light)
  onmousedown=()=>{
    if(document.pointerLockElement===document.body||
    document.mozPointerLockElement===document.body){
    }else{
      document.body.requestPointerLock()
    }
  }
  onmousemove=(event)=>{
    if(document.pointerLockElement===document.body||
    document.mozPointerLockElement===document.body){
      xa-=0.01*event.movementX
      if(-1<ya&&0<event.movementY){
        ya-=0.01*event.movementY
      }
      if(ya<1&&event.movementY<0){
        ya-=0.01*event.movementY
      }
    }
  }
  render=()=>{
    renderer.setSize(innerWidth,innerHeight)
    camera.aspect=innerWidth/innerHeight
    camera.updateProjectionMatrix()
    requestAnimationFrame(render)
    renderer.render(scene,camera)
  }
  render()
  setInterval(()=>{
    light.position.x=camera.position.x
    light.position.y=camera.position.y
    light.position.z=camera.position.z
    camera.lookAt(
      camera.position.x+Math.sin(xa)*Math.cos(ya),
      camera.position.y+Math.sin(ya),
      camera.position.z+Math.cos(xa)*Math.cos(ya)
    )
    if(k[65]){
      camera.position.x+=0.1*Math.cos(xa)
      camera.position.z-=0.1*Math.sin(xa)
    }
    if(k[87]){
      camera.position.x+=0.1*Math.sin(xa)
      camera.position.z+=0.1*Math.cos(xa)
    }
    if(k[68]){
      camera.position.x-=0.1*Math.cos(xa)
      camera.position.z+=0.1*Math.sin(xa)
    }
    if(k[83]){
      camera.position.x-=0.1*Math.sin(xa)
      camera.position.z-=0.1*Math.cos(xa)
    }
    if(k[32]){
      camera.position.y+=0.1
    }
    if(k[88]){
      camera.position.y-=0.1
    }
  },1)
}
</script>

Im wanting to implement a texture image called “dirt.jpg” to my file, but didnt know how…

How are you running your code.. using vite? Or are you double clicking your html file?

Im now using localhost with dirt texture. I want infinite generation:

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
<script>
onload = () => {
  const k = [];
  let xa = 0, ya = 0;
  onkeydown = onkeyup = (e) => { k[e.keyCode] = e.type == "keydown"; };

  document.body.requestPointerLock = document.body.requestPointerLock || document.body.mozRequestPointerLock;
  document.title = "MadDrFrank's Voxels in JS.";
  document.body.style.margin = 0;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 10000);
  const renderer = new THREE.WebGLRenderer();
  document.body.appendChild(renderer.domElement);

  const size = 30;
  const blockSize = 1;
  let blocks = new Set();
  let mesh = null;  // Variable global para el mesh
  const simplex = new SimplexNoise();
  const seed = Math.floor(Math.random() * 10 ** 5);

  camera.position.set(0,0,0)

  function eliminarBloques(scene) {
    if (mesh) {
      scene.remove(mesh);       // Quita el mesh de la escena
      mesh.geometry.dispose();  // Libera la geometrĂ­a
      mesh.material.dispose();  // Libera el material
      mesh = null;              // Limpia la referencia
    }
    blocks = new Set();         // VacĂ­a el conjunto de bloques
  }

  generateDirt=()=>{
    try{
      eliminarBloques(scene)
    }catch(e){}
    for (let x = camera.position.x-size; x < camera.position.x+size; x++) {
      for (let y = camera.position.y-size; y < camera.position.y+size; y++) {
        for (let z = camera.position.z-size; z < camera.position.z+size; z++) {
          if (
            simplex.noise3D(x/25,y/25,z/25+seed)<0.5
            &&
            y<simplex.noise2D(x/25,z/25+seed)*5+simplex.noise2D(x/125,z/125)*25
          ) {
            blocks.add(`${x},${y},${z}`);
          }
        }
      }
    }

    const faceOffsets = [
      { dir: [1, 0, 0], verts: [[0.5, -0.5, -0.5], [0.5, 0.5, -0.5], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5]] },
      { dir: [-1, 0, 0], verts: [[-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, -0.5]] },
      { dir: [0, 1, 0], verts: [[-0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, -0.5], [-0.5, 0.5, -0.5]] },
      { dir: [0, -1, 0], verts: [[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5]] },
      { dir: [0, 0, 1], verts: [[-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], [0.5, 0.5, 0.5], [-0.5, 0.5, 0.5]] },
      { dir: [0, 0, -1], verts: [[0.5, -0.5, -0.5], [-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5]] }
    ];

    const positions = [];
    const indices = [];
    const uvs = [];
    let index = 0;

    for (let x = camera.position.x-size; x < camera.position.x+size; x++) {
      for (let y = camera.position.y-size; y < camera.position.y+size; y++) {
        for (let z = camera.position.z-size; z < camera.position.z+size; z++) {
          const pos = `${x},${y},${z}`;
          if (!blocks.has(pos)) continue;

          for (const { dir, verts } of faceOffsets) {
            const [dx, dy, dz] = dir;
            const neighbor = `${x + dx},${y + dy},${z + dz}`;
            if (!blocks.has(neighbor)) {
              const face = verts.map(([vx, vy, vz]) => [vx + x, vy + y, vz + z]);

              const baseIndex = index;
              face.forEach(v => positions.push(...v));

              // UVs para que la textura se repita por cara
              uvs.push(0, 0);
              uvs.push(1, 0);
              uvs.push(1, 1);
              uvs.push(0, 1);

              indices.push(
                baseIndex, baseIndex + 1, baseIndex + 2,
                baseIndex, baseIndex + 2, baseIndex + 3
              );
              index += 4;
            }
          }
        }
      }
    }

    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
    geometry.setIndex(indices);
    geometry.computeVertexNormals();

    const loader = new THREE.TextureLoader();
    const texture = loader.load("dirt.jpg");
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;

    const material = new THREE.MeshStandardMaterial({ map: texture });
    const mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
  }

  generateDirt()

  setInterval(generateDirt,5000)

  scene.background = new THREE.Color("rgb(0,127,255)");
  scene.fog = new THREE.Fog("rgb(0,127,255)", 100, 500);
  const light = new THREE.PointLight("rgb(255,255,255)", 1, 500);
  scene.add(light);

  onmousedown = () => {
    if (!(document.pointerLockElement === document.body || document.mozPointerLockElement === document.body)) {
      document.body.requestPointerLock();
    }
  };

  onmousemove = (event) => {
    if (document.pointerLockElement === document.body || document.mozPointerLockElement === document.body) {
      xa -= 0.01 * event.movementX;
      ya = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, ya - 0.01 * event.movementY));
    }
  };

  const render = () => {
    renderer.setSize(innerWidth, innerHeight);
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    requestAnimationFrame(render);
    renderer.render(scene, camera);
  };

  const walkSpeed = 5;
  render();

  setInterval(() => {
    light.position.copy(camera.position);
    if(ya<-1.5)ya=-1.5
    if(ya>1.5)ya=1.5
    camera.lookAt(
      camera.position.x + Math.sin(xa) * Math.cos(ya),
      camera.position.y + Math.sin(ya),
      camera.position.z + Math.cos(xa) * Math.cos(ya)
    );
    if (k[65]) {
      camera.position.x += 0.1 * Math.cos(xa) * walkSpeed;
      camera.position.z -= 0.1 * Math.sin(xa) * walkSpeed;
    }
    if (k[87]) {
      camera.position.x += 0.1 * Math.sin(xa) * walkSpeed;
      camera.position.z += 0.1 * Math.cos(xa) * walkSpeed;
    }
    if (k[68]) {
      camera.position.x -= 0.1 * Math.cos(xa) * walkSpeed;
      camera.position.z += 0.1 * Math.sin(xa) * walkSpeed;
    }
    if (k[83]) {
      camera.position.x -= 0.1 * Math.sin(xa) * walkSpeed;
      camera.position.z -= 0.1 * Math.cos(xa) * walkSpeed;
    }
    if (k[69]) camera.position.y += 0.1 * walkSpeed;
    if (k[81]) camera.position.y -= 0.1 * walkSpeed;
  }, 1);
};
</script>

EDIT 1: And performance if its possible…
EDIT 2: I did this another example:

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
<script>
onload = () => {
  const k = [];
  let xa = 0, ya = 0;
  onkeydown = onkeyup = (e) => { k[e.keyCode] = e.type == "keydown"; };

  document.body.requestPointerLock = document.body.requestPointerLock || document.body.mozRequestPointerLock;
  document.title = "MadDrFrank's Voxels in JS.";
  document.body.style.margin = 0;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 10000);
  const renderer = new THREE.WebGLRenderer();
  document.body.appendChild(renderer.domElement);

  const size = 20;
  const blockSize = 1;
  let blocks = new Set();
  mesh = null;  // Variable global para el mesh
  const simplex = new SimplexNoise();
  const seed = Math.floor(Math.random() * 10 ** 5);

  camera.position.set(0,0,0)
  torque={x:0,y:0,z:0}

  generateDirt=()=>{
    try{
      if(mesh){
        scene.remove(mesh)
      }
    }catch(e){
      console.warn(e.message)
    }
    for (let x = camera.position.x-size; x < camera.position.x+size; x++) {
      for (let y = camera.position.y-size; y < camera.position.y+size; y++) {
        for (let z = camera.position.z-size; z < camera.position.z+size; z++) {
          if (
            simplex.noise3D(x/25,y/25,z/25+seed)<0.5
            &&
            y<simplex.noise2D(x/25,z/25+seed)*5+simplex.noise2D(x/125,z/125)*25
          ) {
            blocks.add(`${x},${y},${z}`);
          }
        }
      }
    }

    const faceOffsets = [
      { dir: [1, 0, 0], verts: [[0.5, -0.5, -0.5], [0.5, 0.5, -0.5], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5]] },
      { dir: [-1, 0, 0], verts: [[-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, -0.5]] },
      { dir: [0, 1, 0], verts: [[-0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, -0.5], [-0.5, 0.5, -0.5]] },
      { dir: [0, -1, 0], verts: [[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5]] },
      { dir: [0, 0, 1], verts: [[-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], [0.5, 0.5, 0.5], [-0.5, 0.5, 0.5]] },
      { dir: [0, 0, -1], verts: [[0.5, -0.5, -0.5], [-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5]] }
    ];

    const positions = [];
    const indices = [];
    const uvs = [];
    let index = 0;

    for (let x = camera.position.x-size; x < camera.position.x+size; x++) {
      for (let y = camera.position.y-size; y < camera.position.y+size; y++) {
        for (let z = camera.position.z-size; z < camera.position.z+size; z++) {
          const pos = `${x},${y},${z}`;
          if (!blocks.has(pos)) continue;

          for (const { dir, verts } of faceOffsets) {
            const [dx, dy, dz] = dir;
            const neighbor = `${x + dx},${y + dy},${z + dz}`;
            if (!blocks.has(neighbor)) {
              const face = verts.map(([vx, vy, vz]) => [vx + x, vy + y, vz + z]);

              const baseIndex = index;
              face.forEach(v => positions.push(...v));

              // UVs para que la textura se repita por cara
              uvs.push(0, 0);
              uvs.push(1, 0);
              uvs.push(1, 1);
              uvs.push(0, 1);

              indices.push(
                baseIndex, baseIndex + 1, baseIndex + 2,
                baseIndex, baseIndex + 2, baseIndex + 3
              );
              index += 4;
            }
          }
        }
      }
    }

    geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
    geometry.setIndex(indices);
    geometry.computeVertexNormals();

    loader = new THREE.TextureLoader();
    texture = loader.load("dirt.jpg");
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;

    material = new THREE.MeshStandardMaterial({ map: texture });
    mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
  }

  generateDirt()

  scene.background = new THREE.Color("rgb(0,127,255)");
  scene.fog = new THREE.Fog("rgb(0,127,255)", 100, 500);
  const light = new THREE.PointLight("rgb(255,255,255)", 1, 500);
  scene.add(light);

  onmousedown = () => {
    if (!(document.pointerLockElement === document.body || document.mozPointerLockElement === document.body)) {
      document.body.requestPointerLock();
    }
  };

  onmousemove = (event) => {
    if (document.pointerLockElement === document.body || document.mozPointerLockElement === document.body) {
      xa -= 0.01 * event.movementX;
      ya = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, ya - 0.01 * event.movementY));
    }
  };

  const render = () => {
    renderer.setSize(innerWidth, innerHeight);
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    requestAnimationFrame(render);
    renderer.render(scene, camera);
  };

  const walkSpeed = 5;
  render();

  distance=(obj1,obj2)=>{
    return ((obj1.x-obj2.x)**2+(obj1.y-obj2.y)**2+(obj1.z-obj2.z)**2)**0.5
  }

  setInterval(() => {
    if(distance(torque,camera.position)>5){
      generateDirt()
      torque.x=camera.position.x
      torque.y=camera.position.y
      torque.z=camera.position.z
    }
    light.position.copy(camera.position)
    if(ya<-1.5)ya=-1.5
    if(ya>1.5)ya=1.5
    camera.lookAt(
      camera.position.x + Math.sin(xa) * Math.cos(ya),
      camera.position.y + Math.sin(ya),
      camera.position.z + Math.cos(xa) * Math.cos(ya)
    );
    if (k[65]) {
      camera.position.x += 0.1 * Math.cos(xa) * walkSpeed;
      camera.position.z -= 0.1 * Math.sin(xa) * walkSpeed;
    }
    if (k[87]) {
      camera.position.x += 0.1 * Math.sin(xa) * walkSpeed;
      camera.position.z += 0.1 * Math.cos(xa) * walkSpeed;
    }
    if (k[68]) {
      camera.position.x -= 0.1 * Math.cos(xa) * walkSpeed;
      camera.position.z += 0.1 * Math.sin(xa) * walkSpeed;
    }
    if (k[83]) {
      camera.position.x -= 0.1 * Math.sin(xa) * walkSpeed;
      camera.position.z -= 0.1 * Math.cos(xa) * walkSpeed;
    }
    if (k[69]) camera.position.y += 0.1 * walkSpeed;
    if (k[81]) camera.position.y -= 0.1 * walkSpeed;
  }, 1);
};
</script>

EDIT 3:

I getted a little optimized procedural generation, but with some inner walls that i want to remove. Its possible? Here is the code:

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
<script>
onload = () => {
  // --- ConfiguraciĂłn ---
  const chunkSize = 64; // tamaño del chunk en bloques (más pequeño = menos lag)
  const viewDistanceChunks = 1; // chunks alrededor del jugador
  const simplex = new SimplexNoise();
  const seed = Math.floor(Math.random() * 10 ** 5);

  // --- Setup Three.js ---
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer();
  document.body.appendChild(renderer.domElement);
  document.body.style.margin = "0";
  scene.background = new THREE.Color("rgb(0,127,255)");
  scene.fog = new THREE.Fog("rgb(0,127,255)", 100, 500);

  const light = new THREE.PointLight("rgb(255,255,255)", 1, 500);
  scene.add(light);

  camera.position.set(0, 20, 0);
  let xa = 0, ya = 0;
  const k = [];
  onkeydown = onkeyup = (e) => { k[e.keyCode] = e.type == "keydown"; };

  document.body.requestPointerLock = document.body.requestPointerLock || document.body.mozRequestPointerLock;
  onmousedown = () => {
    if (!(document.pointerLockElement === document.body || document.mozPointerLockElement === document.body)) {
      document.body.requestPointerLock();
    }
  };
  onmousemove = (e) => {
    if (document.pointerLockElement === document.body || document.mozPointerLockElement === document.body) {
      xa -= 0.002 * e.movementX;
      ya = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, ya - 0.002 * e.movementY));
    }
  };

  // --- Variables de chunks ---
  const chunkCache = new Map(); // Guarda bloques de cada chunk
  const chunkMeshes = new Map(); // Guarda meshes de cada chunk

  // --- Helpers ---
  function getChunkKey(x, y, z) {
    return `${x},${y},${z}`;
  }

  // Genera datos de bloques para un chunk dado
  function generateChunkData(cx, cy, cz) {
    const blocks = new Set();
    for (let x = 0; x < chunkSize; x++) {
      for (let y = 0; y < chunkSize; y++) {
        for (let z = 0; z < chunkSize; z++) {
          const worldX = cx * chunkSize + x;
          const worldY = cy * chunkSize + y;
          const worldZ = cz * chunkSize + z;
          if (
            simplex.noise3D(worldX / 25, worldY / 25, worldZ / 25 + seed) < 0.5 &&
            worldY < simplex.noise2D(worldX / 25, worldZ / 25 + seed) * 5 + simplex.noise2D(worldX / 125, worldZ / 125) * 25
          ) {
            blocks.add(`${worldX},${worldY},${worldZ}`);
          }
        }
      }
    }
    return blocks;
  }

  // Crea mesh de un chunk a partir de sus bloques
  function generateChunkMeshFromData(blocks) {
    const faceOffsets = [
      { dir: [1, 0, 0], verts: [[0.5, -0.5, -0.5], [0.5, 0.5, -0.5], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5]] },
      { dir: [-1, 0, 0], verts: [[-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, -0.5]] },
      { dir: [0, 1, 0], verts: [[-0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, 0.5, -0.5], [-0.5, 0.5, -0.5]] },
      { dir: [0, -1, 0], verts: [[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5]] },
      { dir: [0, 0, 1], verts: [[-0.5, -0.5, 0.5], [0.5, -0.5, 0.5], [0.5, 0.5, 0.5], [-0.5, 0.5, 0.5]] },
      { dir: [0, 0, -1], verts: [[0.5, -0.5, -0.5], [-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [0.5, 0.5, -0.5]] }
    ];

    const positions = [];
    const indices = [];
    const uvs = [];
    let index = 0;

    // Para cada bloque, comprobar vecinos para crear caras visibles
    for (const posStr of blocks) {
      const [x, y, z] = posStr.split(",").map(Number);
      for (const { dir, verts } of faceOffsets) {
        const [dx, dy, dz] = dir;
        const neighbor = `${x + dx},${y + dy},${z + dz}`;
        if (!blocks.has(neighbor)) {
          const face = verts.map(([vx, vy, vz]) => [vx + x, vy + y, vz + z]);
          const baseIndex = index;
          face.forEach(v => positions.push(...v));
          uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
          indices.push(baseIndex, baseIndex + 1, baseIndex + 2, baseIndex, baseIndex + 2, baseIndex + 3);
          index += 4;
        }
      }
    }

    if (positions.length === 0) return null;

    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
    geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
    geometry.setIndex(indices);
    geometry.computeVertexNormals();

    const loader = new THREE.TextureLoader();
    const texture = loader.load("dirt.jpg"); // Usa una textura dirt alternativa si quieres
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;

    const material = new THREE.MeshStandardMaterial({ map: texture });

    return new THREE.Mesh(geometry, material);
  }

  // Actualiza chunks visibles según posición de cámara
  function updateChunks() {
    const cx = Math.floor(camera.position.x / chunkSize);
    const cy = Math.floor(camera.position.y / chunkSize);
    const cz = Math.floor(camera.position.z / chunkSize);

    const neededChunks = new Set();

    for (let x = cx - viewDistanceChunks; x <= cx + viewDistanceChunks; x++) {
      for (let y = cy - viewDistanceChunks; y <= cy + viewDistanceChunks; y++) {
        for (let z = cz - viewDistanceChunks; z <= cz + viewDistanceChunks; z++) {
          const key = getChunkKey(x, y, z);
          neededChunks.add(key);

          if (!chunkMeshes.has(key)) {
            if (!chunkCache.has(key)) {
              chunkCache.set(key, generateChunkData(x, y, z));
            }
            const blocks = chunkCache.get(key);
            const mesh = generateChunkMeshFromData(blocks);
            if (mesh) {
              scene.add(mesh);
              chunkMeshes.set(key, mesh);
            }
          }
        }
      }
    }

    // Remover chunks fuera del rango
    for (const key of chunkMeshes.keys()) {
      if (!neededChunks.has(key)) {
        const mesh = chunkMeshes.get(key);
        scene.remove(mesh);
        mesh.geometry.dispose();
        mesh.material.dispose();
        chunkMeshes.delete(key);
        chunkCache.delete(key); // también borra la data para liberar memoria, opcional
      }
    }
  }

  // --- Loop de render ---
  function animate() {
    requestAnimationFrame(animate);
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    // Control de cámara (WASD + QE)
    const walkSpeed = 0.2;
    if (k[87]) { // W
      camera.position.x += Math.sin(xa) * walkSpeed;
      camera.position.z += Math.cos(xa) * walkSpeed;
    }
    if (k[83]) { // S
      camera.position.x -= Math.sin(xa) * walkSpeed;
      camera.position.z -= Math.cos(xa) * walkSpeed;
    }
    if (k[65]) { // A
      camera.position.x += Math.cos(xa) * walkSpeed;
      camera.position.z -= Math.sin(xa) * walkSpeed;
    }
    if (k[68]) { // D
      camera.position.x -= Math.cos(xa) * walkSpeed;
      camera.position.z += Math.sin(xa) * walkSpeed;
    }
    if (k[69]) camera.position.y += walkSpeed; // E
    if (k[81]) camera.position.y -= walkSpeed; // Q

    // Actualizar luz para que siga cámara
    light.position.copy(camera.position);

    // Control de rotaciĂłn
    if (ya < -1.5) ya = -1.5;
    if (ya > 1.5) ya = 1.5;
    camera.lookAt(
      camera.position.x + Math.sin(xa) * Math.cos(ya),
      camera.position.y + Math.sin(ya),
      camera.position.z + Math.cos(xa) * Math.cos(ya)
    );

    animateChunksIfNeeded();

    renderer.render(scene, camera);
  }

  let lastUpdate = 0;
  function animateChunksIfNeeded() {
    const now = performance.now();
    if (now - lastUpdate > 500) { // actualizar cada 0.5s para no bloquear mucho
      updateChunks();
      lastUpdate = now;
    }
  }

  animate();
};
</script>

And it gets laggy when the terrain generates… any solution?

This is my actual approach with different textures per side of a cube. Its runs slowlier than the basic example, but i cant do anything…