mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
103.06k stars 35.41k forks source link

wrong z order when rendering material with alpha mode "BLEND" #28157

Closed am05mhz closed 7 months ago

am05mhz commented 7 months ago

Description

as you can see in the attached image, the tree behind is rendered on top of the tree in front of it. this behavior is also reflected on https://gltf-viewer.donmccurdy.com/ and https://sandbox.babylonjs.com/ but its rendered correctly on https://sketchfab.com/3d-models/forest-house-52429e4ef7bf4deda1309364a2cda86f Screenshot 2024-04-19 011445

Reproduction steps

  1. download https://sketchfab.com/3d-models/forest-house-52429e4ef7bf4deda1309364a2cda86f glb file
  2. load it to https://gltf-viewer.donmccurdy.com/ or https://sandbox.babylonjs.com/ or your own project
  3. on babylon, you can play with the TreeLeaf material and change its alpha mode to alpha test, it will render correctly but with no alpha blending

Code

code is in Vue 3

<script setup lang="js">
import { onMounted, onBeforeUnmount, ref, computed } from 'vue'
import * as THREE from 'three'
import Stats from 'stats.js'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'

var scene, camera, renderer, house, shiba, controls, effect
var cx, cy, cz
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const ratio = computed(() => width.value / height.value)

const stats = new Stats()
const canvas = ref(null)

const init = () => {
  scene = new THREE.Scene()
  camera = new THREE.PerspectiveCamera(45, ratio.value, 0.125, 1000)
  cx = -0.4
  cy = 0.4
  cz = 0.6
  camera.position.set(cx, cy, cz)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setClearColor('#e5e5e5')
  renderer.setSize(width.value, height.value)

  effect = new EffectComposer(renderer)
  const renderPass = new RenderPass(scene, camera)
  effect.addPass(renderPass)

  controls = new OrbitControls(camera, renderer.domElement)
  scene.add(new THREE.GridHelper(5, 50, 0xcccccc, 0xdddddd))

  canvas.value.append(renderer.domElement)
  canvas.value.append(stats.dom)

  const loader = new GLTFLoader()
  loader.load('models/forest-house.gltf', async (gltf) => {
    house = gltf.scene
    await renderer.compileAsync(house, camera, scene)
    house.scale.set(3, 3, 3)
    scene.add(house)
  }, (xhr) => {
    console.log('house', (xhr.loaded / xhr.total * 100) + '% loaded')
  }, function (error) {
    console.error(error)
  })
  loader.load('models/shiba-small.gltf', async (gltf) => {
    shiba = gltf.scene
    await renderer.compileAsync(shiba, camera, scene)
    shiba.position.set(-0.19, 0.0235, 0)
    shiba.rotation.y = 0.2
    shiba.scale.set(.1, .1, .1)
    scene.add(shiba)
  }, (xhr) => {
    console.log('shiba', (xhr.loaded / xhr.total * 100) + '% loaded')
  }, function (error) {
    console.error(error)
  })

  window.addEventListener('resize', resize)

  render()
}

const resize = (ev) => {
  width.value = window.innerWidth
  height.value = window.innerHeight
  camera.aspect = ratio.value
  camera.updateProjectionMatrix()

  if (renderer){
    renderer.setSize(width.value, height.value)
  }
}

const animate = () => {
  stats.update()
}

const render = () => {
  requestAnimationFrame(render)
  if (effect){
    effect.render()
  } else if (renderer){
    renderer.render(scene, camera)
  }
  controls.update()
  animate()
}

onMounted(() => {
  init()
})

onBeforeUnmount(() => {
  renderer.forceContextLoss();
  renderer.context = null;
  renderer.domElement = null;
  renderer = null;
})
</script>

<template>
  <div class="threejs" ref="canvas"></div>
</template>

Live example

live example can just load the file on step 1, to the preview tool on step 2

Screenshots

Screenshot 2024-04-19 011445

Version

^0.162.0

Device

Desktop

Browser

Chrome

OS

Windows

donmccurdy commented 7 months ago

Realtime 3D engines typically can sort objects - not triangles or pixels - making this category of problem in alpha blending a known limitation. Entire scenes should not use alpha blending without special care for the sort order, or other techniques like OIT or alpha hashing.

RemusMar commented 7 months ago

@am05mhz To get much better results you can use this trick:

        loader.load( 'meshes/forest_house.glb', function ( gltf ) {
            gltf.scene.traverse(function (child) {
                if (child.isMesh) {
                    if (child.material.transparent === true) {
                        child.material.depthWrite = true;
                        child.material.alphaTest = 0.5;
                    }
                }
            });

Here is the result: https://necromanthus.com/Test/html5/forest_house.html

@donmccurdy

Realtime 3D engines typically can sort objects - not triangles or pixels - making this category of problem in alpha blending a known limitation.

Don, there is an uninspired (I don't want to say wrong) setting inside of the GLTF Loader: materialParams.depthWrite = false; That will generate an alpha sorting mess in 99% of the scenarios. You should study this sample as well: https://necromanthus.com/Test/html5/head.html Click on the stage to see the 3 cases. cheers

Mugen87 commented 7 months ago

We actually use an updated version of the asset in https://threejs.org/examples/webgl_loader_gltf_avif

Mugen87 commented 7 months ago

Closing. Considering this as a modeling issue.

RemusMar commented 7 months ago

Considering this as a modeling issue.

It's not Michael. See my above result with that (not very well designed) sample.

am05mhz commented 7 months ago

@am05mhz To get much better results you can use this trick:

      loader.load( 'meshes/forest_house.glb', function ( gltf ) {
          gltf.scene.traverse(function (child) {
              if (child.isMesh) {
                  if (child.material.transparent === true) {
                      child.material.depthWrite = true;
                      child.material.alphaTest = 0.5;
                  }
              }
          });

Here is the result: https://necromanthus.com/Test/html5/forest_house.html

@donmccurdy

Realtime 3D engines typically can sort objects - not triangles or pixels - making this category of problem in alpha blending a known limitation.

Don, there is an uninspired (I don't want to say wrong) setting inside of the GLTF Loader: materialParams.depthWrite = false; That will generate an alpha sorting mess in 99% of the scenarios. You should study this sample as well: https://necromanthus.com/Test/html5/head.html Click on the stage to see the 3 cases. cheers

thanks, this solves my problem

RemusMar commented 7 months ago

@Mugen87

We actually use an updated version of the asset in https://threejs.org/examples/webgl_loader_gltf_avif

I get exactly that result (in fact mine is a bit better) using the initial asset and this runtime change:

            gltf.scene.traverse(function (child) {
                if (child.isMesh) {
                    if (child.material.name === "TreeLeafs") {
                        child.material.side = THREE.DoubleSide;
                        child.material.depthWrite = true;
                        child.material.depthTest = true;
                        child.material.alphaTest = 0.4;
                        child.material.transparent = false;
                    }
                }
            });

https://necromanthus.com/Test/html5/forest_house.html And you'll find a similar modified material in the "fixed" asset. So Michael, it was a hack, not a real fix. cheers