Search code examples
javascriptoptimizationthree.jsweb-frontend

Most resource efficient way to create a beach umbrella in Three.js


I'm new to Three.js, I find it great because with very low effort I've been able to create a simple scene. I'm working on a beach scene and I'd like to add 10 thousands of beach umbrella to the scene. I've been able to do so easily using cones to represent canopy and cylinders for poles. Now to make the scene more realistic, I'd like to make the canopy of my umbrella with stripes, as you see in very common beach umbrellas. I understand there are multiple ways I can do that:

  • Using a texture
  • changing material for each face
  • creating each slide as a separate mesh

Since each of these options require some work, I'd like to understand which one is the best to keep the scene lightweight considering I'm planning to add around 10 thousands of umbrellas to my scene.

Thanks for your help


Solution

  • Give the instancing a try:

    body{
      overflow: hidden;
      margin: 0;
    }
    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/[email protected]/build/three.webgpu.js",
          "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
        }
      }
    </script>
    <script type="module">
    import * as THREE from "three";
    import {uv, vec2, vec3, fract, attribute, select} from "three";
    import {OrbitControls} from "three/addons/controls/OrbitControls.js";
    import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
    
    console.clear();
    
    let scene = new THREE.Scene();
    scene.background = new THREE.Color("skyblue");
    let camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 1000);
    camera.position.set(0, 1, 1).setLength(10);
    let renderer = new THREE.WebGPURenderer({antialias: true});
    renderer.setPixelRatio( devicePixelRatio );
    renderer.setSize( innerWidth, innerHeight );
    document.body.appendChild(renderer.domElement);
    
    window.addEventListener("resize", event => {
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(innerWidth, innerHeight);
    })
    
    let controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    
    let light = new THREE.DirectionalLight(0xffffff, Math.PI);
    light.position.setScalar(1);
    scene.add(light, new THREE.AmbientLight(0xffffff, Math.PI * 0.5));
    
    let circle = 100;
    
    let sand = new THREE.Mesh(
      new THREE.CircleGeometry(circle, 32).rotateX(-Math.PI * 0.5),
      new THREE.MeshLambertMaterial({color: "#F6DCBD"})
    );
    scene.add(sand);
    
    let gs = [
      new THREE.CylinderGeometry(0.025, 0.025, 0.9, 3, 1, true).translate(0, 0.45, 0),
      new THREE.LatheGeometry([
        [1, 0.8], [0.66, 0.9], [0.33, 0.96], [0, 1]
      ].map(p => {return new THREE.Vector2(...p)}),
      8)
    ];
    gs.forEach((g, gIdx) => {
      g.setAttribute("color", new THREE.Float32BufferAttribute(new Array(g.attributes.position.count * 3).fill(gIdx), 3));
    })
    let g = mergeGeometries(gs);
    let m = new THREE.MeshLambertNodeMaterial({vertexColors: true});
    let amount = 10000;
    let io = new THREE.InstancedMesh(g, m, amount);
    
    let dummy = new THREE.Object3D();
    let dummyColor = new THREE.Color();
    let instColor = [];
    for(let i = 0; i < amount; i++){
      dummy.position.setFromCylindricalCoords(Math.sqrt(circle * circle * Math.random()), Math.random() * 2 * Math.PI, 0);
      dummy.rotation.y = Math.PI * 2 * Math.random();
      dummy.rotation.z = Math.PI * 0.05 * Math.sign(Math.random() - 0.5);
      dummy.updateMatrix();
      io.setMatrixAt(i, dummy.matrix);
      dummyColor.setHSL(Math.random(), 0.75, 0.5);
      instColor.push(dummyColor.r, dummyColor.g, dummyColor.b);
    }
    g.setAttribute("instColor", new THREE.InstancedBufferAttribute(new Float32Array(instColor), 3));
    
    // TLS magic is here
    let uvScaled = uv().mul(vec2(1., 3.)).toVar();
    let iColor = attribute("instColor").toVar();
    let col = select(fract(uvScaled.y).greaterThan(0.5), vec3(1., 1., 1.), iColor).toVar();
    m.colorNode = col;
    
    scene.add(io);
    
    renderer.setAnimationLoop(() => {
      controls.update();
      renderer.render(scene, camera);
    })
    </script>