mkkellogg / GaussianSplats3D

Three.js-based implementation of 3D Gaussian splatting
MIT License
1.08k stars 130 forks source link

Problem with semi-transparent mesh geometry #257

Open tingyus839 opened 1 week ago

tingyus839 commented 1 week ago

Hello, thanks for the great library! The library currently works well with other mesh geometry-based models in the scene. However, if the material of an object in the scene is semi-transparent (i.e. with transparent=true), the depth test result seems to be incorrect. Here is a video demonstrating the issue: image

The area marked by the green pen should not have the red tint since those 3DGS splats are in fact outside of the box. The problem seems to be related to the fact that WebGLRenderer of Three.js renders transparent objects separately and sort based on the distance to the camera. I've tried changing renderOrder of GaussianSplats3D.DropInViewer or the box object or changing depthTest property on the material of the box but still does not get correct result. Any help is appreciated.

Minimum reproducible example modified from demo/dropin.html:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>3D Gaussian Splats - Drop-in example</title>
  <script type="text/javascript" src="js/util.js"></script>
  <script type="importmap">
    {
        "imports": {
            "three": "./lib/three.module.js",
            "@mkkellogg/gaussian-splats-3d": "./lib/gaussian-splats-3d.module.js"
        }
    }
  </script>
  <style>
    body {
      background-color: #000000;
      height: 100vh;
      margin: 0px;
    }
  </style>

</head>

<body>
  <button id="add-btn-solid">Add Solid Box</button>
  <button id="add-btn-semi">Add Semi-transparent Box</button>
  <script type="module">
    import * as GaussianSplats3D from '@mkkellogg/gaussian-splats-3d';
    import * as THREE from 'three';

    function setupRenderer() {
      const renderWidth = 800;
      const renderHeight = 600;

      const rootElement = document.createElement('div');
      rootElement.style.width = renderWidth + 'px';
      rootElement.style.height = renderHeight + 'px';
      rootElement.style.position = 'relative';
      rootElement.style.left = '50%';
      rootElement.style.top = '50%';
      rootElement.style.transform = 'translate(-50%, -50%)';
      document.body.appendChild(rootElement);

      const renderer = new THREE.WebGLRenderer({
        antialias: false,
        clearColor: 'white'
      });
      renderer.setSize(renderWidth, renderHeight);
      rootElement.appendChild(renderer.domElement);

      return {
        'renderer': renderer,
        'renderWidth': renderWidth,
        'renderHeight': renderHeight
      }
    }

    function setupCamera(renderWidth, renderHeight) {
      const camera = new THREE.PerspectiveCamera(65, renderWidth / renderHeight, 0.1, 500);
      camera.position.copy(new THREE.Vector3().fromArray([-1, -4, 6]));
      camera.lookAt(new THREE.Vector3().fromArray([0, 4, -0]));
      camera.up = new THREE.Vector3().fromArray([0, -1, -0.6]).normalize();
      return camera;
    }

    function setupThreeScene() {
      const threeScene = new THREE.Scene();
      const boxColor = 0xBBBBBB;
      const boxGeometry = new THREE.BoxGeometry(2, 2, 2);
      const boxMesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({ 'color': boxColor }));
      threeScene.add(boxMesh);
      boxMesh.position.set(3, 2, 2);
      return threeScene;
    }

    function setupControls(camera, renderer) {
      const controls = new GaussianSplats3D.OrbitControls(camera, renderer.domElement);
      controls.rotateSpeed = 0.5;
      controls.maxPolarAngle = Math.PI * .75;
      controls.minPolarAngle = 0.1;
      controls.enableDamping = true;
      controls.dampingFactor = 0.05;
      return controls;
    }

    const { renderer, renderWidth, renderHeight } = setupRenderer();
    const camera = setupCamera(renderWidth, renderHeight);
    const threeScene = setupThreeScene();
    const controls = setupControls(camera, renderer);

    const viewer = new GaussianSplats3D.DropInViewer({
      gpuAcceleratedSort: false,
      sharedMemoryForWorkers: false,
      selfDrivenMode: false,
      dynamicScene: true,
      sceneRevealMode: GaussianSplats3D.SceneRevealMode.Instant,
      logLevel: GaussianSplats3D.LogLevel.Debug
    });
    viewer.addSplatScenes([
      {
        'path': 'assets/lego.ply',
        'splatAlphaRemovalThreshold': 20,
      },
    ], true);
    threeScene.add(viewer);

    const light = new THREE.PointLight(0xff0000, 100, 100);
    light.position.set(-3, -3, -3);
    threeScene.add(light);

    requestAnimationFrame(update);
    function update() {
      requestAnimationFrame(update);
      controls.update();
      renderer.render(threeScene, camera);
    }

    const btnSolid = document.querySelector("#add-btn-solid")
    btnSolid.addEventListener('click', e => {
      const geometry = new THREE.BoxGeometry(1, 1, 1);
      const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
      const cube = new THREE.Mesh(geometry, material);
      cube.position.set(0, -0.2, 0.8);
      threeScene.add(cube);
    })

    const btnSemi = document.querySelector("#add-btn-semi")
    btnSemi.addEventListener('click', e => {
      const geometry = new THREE.BoxGeometry(1, 1, 1);
      const material = new THREE.MeshStandardMaterial({ color: 0xff0000, transparent: true, opacity: 0.5 });
      const cube = new THREE.Mesh(geometry, material);
      cube.position.set(0, -0.2, 0.8);
      threeScene.add(cube);
    })

  </script>
</body>

</html>
mkkellogg commented 1 week ago

The problem is that both the splats and the cube are transparent objects and rendering transparent objects that overlap can be difficult, since transparent objects don't write to the depth buffer (and if they did you'd still get incorrect results). You'd have the same problem rendering multiple transparent cubes if they overlap in 3D space. There isn't really a great solution to this problem in general (at least when splats are involved). For a scene with just transparent meshes, you could use something like depth peeling, but it's a complex thing to implement. Unfortunately for now, only opaque meshes are properly supported.