I generated a glTF file with two boxes each having a transparent texture (the .png images are in RGBA format).
I have experienced on multiple occasions, that this glTF is rendered incorrectly. That is when I look at it from the top everything is rendered as it should, but when I look at it from the bottom, it seems that the box which should be in the background is rendered on top of the closer box:
I observed this behavior when I used a fully opaque RGBA texture
- In the previewer of the VSCode glTF extension with the
Three.js
engine (but not withBabylon.js
andFilament
- Three.js glTF viewer: gltf-viewer.donmccurdy.com
The effect is more subtle with an alpha value < 1, but still the rendering order is wrong, when looking from the bottom:
However in two of the glTF files worked when using https://sandbox.babylonjs.com/, only opaque-texture-atlas.gltf failed (the one that can be found below).
Now I am asking myself if this is a bug in Three.js
or if my glTF model is not constructed correctly (I build it myself in code). For example I suspected that the normals of the bottom faces of the boxes are wrong (currently (0, -1, 0)
) or that I missed something about "alphaMode" : "MASK"
.
To further test this I build my own little viewer in Three.js
and was able to reproduce this with version r.126
and the opaque glTF (but interestingly not with the partially transparent one).
var camera, controls, scene, renderer;
init();
animate();
function init() {
// renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// camera
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 1000000);
camera.position.set(4, 4, 6);
camera.up.set(0, 1, 0);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// scene
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xdcdcdc );
//lights
var pointLight1 = new THREE.PointLight(0xffffff, 5, 1000, 2);
pointLight1.position.set(30, 30, 40);
pointLight1.castShadow = true;
scene.add(pointLight1);
var pointLight2 = new THREE.PointLight(0xffffff, 1, 1000, 2);
pointLight2.position.set(-30, -30, -40);
pointLight2.castShadow = true;
scene.add(pointLight2);
var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(0, 20, 40);
directionalLight.castShadow = true;
scene.add(directionalLight);
//controls
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 3.0;
controls.zoomSpeed = 6.8;
controls.panSpeed = 9.0;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [ 65, 83, 68 ];
controls.addEventListener('change', render);
// load gltf model and texture
const loader = new THREE.GLTFLoader();
loader.load('https://gist.githubusercontent.com/FrankenApps/0fe05e47f148cacb38e2509640e1aa54/raw/fad6897ed11d9e7ad8b6f24400b3bbd5bf2849d1/opaque-texture-atlas.gltf', (gltf) => {
gltf.scene.scale.set(1, 1, 1);
var mesh = gltf.scene;
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
});
window.addEventListener('resize', onWindowResize, false);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
controls.handleResize();
renderer.render(scene, camera);
}
function render() {
renderer.render(scene, camera);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
render();
}
html,body {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#canvas {
width: 100%;
height: 100%;
touch-action: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r126/three.min.js"></script>
<script src="https://unpkg.com/three@0.126.0/examples/js/controls/TrackballControls.js"></script>
<script src="https://unpkg.com/three@0.126.0/examples/js/loaders/GLTFLoader.js"></script>
Update
Thanks to @WestLangley I basically know that my problem is related to meshes
having no offset. That is true, but basically my whole idea as to why I even tried to build a .gltf. The reason is, I wanted to minimize draw calls for performance. So I basically I calculate vertex positions and then build the objects. In order for this to work, I will need as much vertecies per primitive
as possible. I then also calculate the correct uv coordinates and grab the texture of the "vertex group" / "triangle mesh" from a texture atlas. Unfortunately, GPUs have a maximum texture size, which means that if my texture atlas is full I go ahead and create another mesh + primitive (at the cost of a draw call).
The idea can be seen in the new example below
{
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"count": 72,
"componentType": 5126,
"extras": {},
"type": "VEC3",
"min": [
-0.75,
-0.5,
-0.25
],
"max": [
0.75,
3.5,
0.25
]
},
{
"bufferView": 0,
"byteOffset": 12,
"count": 72,
"componentType": 5126,
"extras": {},
"type": "VEC3"
},
{
"bufferView": 0,
"byteOffset": 24,
"count": 72,
"componentType": 5126,
"extras": {},
"type": "VEC2"
},
{
"bufferView": 1,
"byteOffset": 0,
"count": 72,
"componentType": 5125,
"extras": {},
"type": "SCALAR"
}
],
"asset": {
"extras": {},
"version": "2.0"
},
"buffers": [
{
"byteLength": 2592,
"uri": "data:application/octet-stream;base64,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",
"extras": {}
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 2592,
"byteStride": 32,
"name": "Cube0",
"target": 34962,
"extras": {}
},
{
"buffer": 0,
"byteLength": 288,
"byteOffset": 2304,
"name": "Indices0",
"extras": {}
}
],
"extras": {},
"images": [
{
"mimeType": "image/png",
"uri": "data:image/png;base64,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",
"extras": {}
}
],
"materials": [
{
"alphaMode": "BLEND",
"doubleSided": false,
"pbrMetallicRoughness": {
"baseColorFactor": [
1.0,
1.0,
1.0,
1.0
],
"baseColorTexture": {
"index": 0,
"texCoord": 0,
"extras": {}
},
"metallicFactor": 1.0,
"roughnessFactor": 1.0,
"extras": {}
},
"emissiveFactor": [
0.0,
0.0,
0.0
],
"extras": {}
}
],
"meshes": [
{
"extras": {},
"primitives": [
{
"attributes": {
"TEXCOORD_0": 2,
"POSITION": 0,
"NORMAL": 1
},
"extras": {},
"indices": 3,
"material": 0
}
]
}
],
"nodes": [
{
"extras": {},
"mesh": 0
}
],
"scenes": [
{
"extras": {},
"nodes": [
0
]
}
],
"textures": [
{
"source": 0,
"extras": {}
}
]
}
All glTF files used can be found in this gist.
As you can see when looking from below, the upper box shines through, even though both RGBA .png textures have an alpha value of 255. Also this does not happen when looking at the model from the top.
I checked the options provided in the link @WestLangley provided, but unfortunately they did not help me because in most cases (except for when my texture atlas is full) I will have to deal with a single object. So is there maybe an option that forces the renderer to check the depth per vertex or per triangle or something similar?