I am new in THREEJS. In the past, I have used AFRAME, CESIUM, XEOGL and BABYLONJS. But eventually, due to memory consumption and performance, I have realized that the best product for making a CAD visualizer is THREEJS.
BABYLONJS takes more than 4 minutes to load a big GLTF file (400MB) while THREEJS takes only 30 secs. The memory taken by BABYLONJS is 4 times that used by THREEJS.
I know that there are still some issues to be able to create instances (GPU) from a loaded GLTF file in THREEJS, but I only need to change the position and rotation in each instance, no need to animate anything.
I have tried with GLTF1.0 and GLTF2.0 and the issue is the same. When I load the GLTF model, I get a scene. From this scene I am trying to obtain the buffergeometry from children array. However when I try to créate an instance, it is not working. My objects are static (no animations at all).
Is there any way to create an instance of an Object3D or from its buffergeometry?
In BABYLONJS it is very simple to create instances from a loaded GLTF file. I really need to use instances to save RAM and use GPU instead CPU resources. My scene needs to load many copies of the same objects to compound the scene.
Some problems I see using GLFT loader:
var geo = data.scene.children[0].children[0].children[0].children[0].geometry
10000 objects, multimaterial --> 4FPS
10000 objects, singlematerial--> 4FPS
10000 objects, instanced --> 4FPS
10000 objects, merged --> Not working
Here you have my code using Duck GLTF example:
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - interactive instances (gpu)</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
/>
<style>
body {
font-family: Monospace;
background-color: #f0f0f0;
margin: 0px;
overflow: hidden;
}
.info {
position: absolute;
background-color: black;
opacity: 0.8;
color: white;
text-align: center;
top: 0px;
width: 100%;
}
.info a {
color: #00ffff;
}
#notSupported {
width: 50%;
margin: auto;
border: 2px red solid;
margin-top: 20px;
padding: 10px;
}
</style>
</head>
<body>
<div class="info">
<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a>
webgl - gpu picking of geometry instances
<div id="notSupported" style="display: none">
Sorry your graphics card + browser does not support hardware instancing
</div>
<br /><br />
<div>
This demo compares different methods of constructing and rendering many
instances of a single geometry.
</div>
<br />
<div>
<div style="display: inline-block">
<span>number of<br />geometry instances</span>
<br />
<select id="instanceCount">
<option>100</option>
<option>500</option>
<option selected>1000</option>
<option>2000</option>
<option>3000</option>
<option>5000</option>
<option>10000</option>
<option>20000</option>
<option>30000</option>
<option>50000</option>
<option>100000</option>
</select>
</div>
<div style="display: inline-block">
<span>method of<br />construction/rendering</span>
<br />
<select id="method">
<option>instanced</option>
<option>merged</option>
<option selected>singleMaterial</option>
<option>multiMaterial</option>
</select>
</div>
<div style="display: inline-block">
<span>render continuously<br />(to get fps reading)</span>
<br />
<input id="animate" type="checkbox" />
</div>
<div style="display: inline-block">
<span
>use override material<br />(only effects singleMaterial
method)</span
>
<br />
<input id="override" type="checkbox" checked />
</div>
<div style="display: inline-block">
<span>construct anew<br />(to get additional timings)</span>
<br />
<button id="construct" type="button">do it</button>
</div>
</div>
<br />
<div>
<span>Materials: #<span id="materialCount"></span></span>
<span>Objects: #<span id="objectCount"></span></span>
<span>Drawcalls: #<span id="drawcalls"></span></span>
<span>Construction time: <span id="initTime"></span> ms</span>
</div>
</div>
<div id="container"></div>
<script src="../build/three.js"></script>
<script src="js/controls/TrackballControls.js"></script>
<script src="js/libs/stats.min.js"></script>
<script src="js/loaders/GLTF2Loader.js"></script>
<script id="vertMerged" type="x-shader/x-vertex">
#define SHADER_NAME vertMerged
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
#ifdef PICKING
attribute vec3 pickingColor;
#else
attribute vec3 color;
varying vec3 vPosition;
#endif
varying vec3 vColor;
void main() {
vec3 positionEye = ( modelViewMatrix * vec4( position, 1.0 ) ).xyz;
#ifdef PICKING
vColor = pickingColor;
#else
vColor = color;
vPosition = positionEye;
#endif
gl_Position = projectionMatrix * vec4( positionEye, 1.0 );
}
</script>
<script id="fragMerged" type="x-shader/x-fragment">
#define SHADER_NAME fragMerged
#extension GL_OES_standard_derivatives : enable
precision highp float;
varying vec3 vColor;
#ifndef PICKING
varying vec3 vPosition;
#endif
void main() {
#ifdef PICKING
gl_FragColor = vec4( vColor, 1.0 );
#else
vec3 fdx = dFdx( vPosition );
vec3 fdy = dFdy( vPosition );
vec3 normal = normalize( cross( fdx, fdy ) );
float diffuse = dot( normal, vec3( 0.0, 0.0, 1.0 ) );
gl_FragColor = vec4( diffuse * vColor, 1.0 );
#endif
}
</script>
<script id="vertInstanced" type="x-shader/x-vertex">
#define SHADER_NAME vertInstanced
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec3 mcol0;
attribute vec3 mcol1;
attribute vec3 mcol2;
attribute vec3 mcol3;
#ifdef PICKING
attribute vec3 pickingColor;
#else
attribute vec3 color;
varying vec3 vPosition;
#endif
varying vec3 vColor;
void main() {
mat4 matrix = mat4(
vec4( mcol0, 0 ),
vec4( mcol1, 0 ),
vec4( mcol2, 0 ),
vec4( mcol3, 1 )
);
vec3 positionEye = ( modelViewMatrix * matrix * vec4( position, 1.0 ) ).xyz;
#ifdef PICKING
vColor = pickingColor;
#else
vColor = color;
vPosition = positionEye;
#endif
gl_Position = projectionMatrix * vec4( positionEye, 1.0 );
}
</script>
<script id="fragInstanced" type="x-shader/x-fragment">
#define SHADER_NAME fragInstanced
#extension GL_OES_standard_derivatives : enable
precision highp float;
varying vec3 vColor;
#ifndef PICKING
varying vec3 vPosition;
#endif
void main() {
#ifdef PICKING
gl_FragColor = vec4( vColor, 1.0 );
#else
vec3 fdx = dFdx( vPosition );
vec3 fdy = dFdy( vPosition );
vec3 normal = normalize( cross( fdx, fdy ) );
float diffuse = dot( normal, vec3( 0.0, 0.0, 1.0 ) );
gl_FragColor = vec4( diffuse * vColor, 1.0 );
#endif
}
</script>
<script id="vertMaterial" type="x-shader/x-vertex">
#define SHADER_NAME vertMaterial
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
#ifndef PICKING
varying vec3 vPosition;
#endif
void main() {
vec3 positionEye = ( modelViewMatrix * vec4( position, 1.0 ) ).xyz;
#ifndef PICKING
vPosition = positionEye;
#endif
gl_Position = projectionMatrix * vec4( positionEye, 1.0 );
}
</script>
<script id="fragMaterial" type="x-shader/x-fragment">
#define SHADER_NAME fragMaterial
#extension GL_OES_standard_derivatives : enable
precision highp float;
#ifdef PICKING
uniform vec3 pickingColor;
#else
uniform vec3 color;
varying vec3 vPosition;
#endif
void main() {
#ifdef PICKING
gl_FragColor = vec4( pickingColor, 1.0 );
#else
vec3 fdx = dFdx( vPosition );
vec3 fdy = dFdy( vPosition );
vec3 normal = normalize( cross( fdx, fdy ) );
float diffuse = dot( normal, vec3( 0.0, 0.0, 1.0 ) );
gl_FragColor = vec4( diffuse * color, 1.0 );
#endif
}
</script>
<script>
var container, stats;
var camera, controls, scene, renderer;
var pickingData, pickingRenderTarget, pickingScene;
var useOverrideMaterial = true;
var singleMaterial, singlePickingMaterial;
var highlightBox;
var materialList = [];
var geometryList = [];
var objectCount = 0;
var geometrySize;
var mouse = new THREE.Vector2();
var scale = 1.03;
var loader = new THREE.GLTF2Loader();
var pixelBuffer = new Uint8Array(4);
var instanceCount, method, doAnimate;
gui();
init();
initMesh();
if (doAnimate) animate();
function gui() {
var instanceCountElm = document.getElementById("instanceCount");
instanceCount = parseInt(instanceCountElm.value);
instanceCountElm.addEventListener("change", function () {
instanceCount = parseInt(instanceCountElm.value);
initMesh();
});
var methodElm = document.getElementById("method");
method = methodElm.value;
methodElm.addEventListener("change", function () {
method = methodElm.value;
initMesh();
});
var animateElm = document.getElementById("animate");
doAnimate = animateElm.checked;
animateElm.addEventListener("click", function () {
doAnimate = animateElm.checked;
animate();
});
var overrideElm = document.getElementById("override");
useOverrideMaterial = overrideElm.checked;
overrideElm.addEventListener("click", function () {
useOverrideMaterial = overrideElm.checked;
initMesh();
});
var constructElm = document.getElementById("construct");
constructElm.addEventListener("click", function () {
initMesh();
});
}
function clean() {
THREE.Cache.clear();
materialList.forEach(function (m) {
m.dispose();
});
geometryList.forEach(function (g) {
g.dispose();
});
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
scene.add(camera);
scene.add(highlightBox);
pickingScene = new THREE.Scene();
pickingData = {};
materialList = [];
geometryList = [];
objectCount = 0;
singleMaterial = undefined;
singlePickingMaterial = undefined;
}
var randomizeMatrix = (function () {
var position = new THREE.Vector3();
var rotation = new THREE.Euler();
var quaternion = new THREE.Quaternion();
var scale = new THREE.Vector3();
return function (matrix) {
position.x = Math.random() * 40 - 20;
position.y = Math.random() * 40 - 20;
position.z = Math.random() * 40 - 20;
rotation.x = Math.random() * 2 * Math.PI;
rotation.y = Math.random() * 2 * Math.PI;
rotation.z = Math.random() * 2 * Math.PI;
quaternion.setFromEuler(rotation, false);
scale.x = scale.y = scale.z = 0.001;
matrix.compose(position, quaternion, scale);
};
})();
function initMesh() {
clean();
loader.load("models/gltf/Duck/glTF-Binary/Duck.glb", function (data) {
console.log(data);
var geo =
data.scene.children[0].children[0].children[0].children[0].geometry;
console.log("geo:");
console.log(geo);
geo.computeBoundingBox();
geometrySize = geo.boundingBox.getSize();
geometryList.push(geo);
var start = window.performance.now();
switch (method) {
case "merged":
makeMerged(geo);
break;
case "instanced":
makeInstanced(geo);
break;
case "singleMaterial":
makeSingleMaterial(geo);
break;
case "multiMaterial":
makeMultiMaterial(geo);
break;
}
render();
var end = window.performance.now();
document.getElementById("materialCount").innerText =
materialList.length;
document.getElementById("objectCount").innerText = objectCount;
document.getElementById("drawcalls").innerText =
renderer.info.render.calls;
document.getElementById("initTime").innerText = (end - start).toFixed(
2
);
});
}
function makeMultiMaterial(geo) {
var vert = document.getElementById("vertMaterial").textContent;
var frag = document.getElementById("fragMaterial").textContent;
var material = new THREE.RawShaderMaterial({
vertexShader: vert,
fragmentShader: frag,
uniforms: {
color: {
value: new THREE.Color(),
},
},
});
var pickingMaterial = new THREE.RawShaderMaterial({
vertexShader: "#define PICKING\n" + vert,
fragmentShader: "#define PICKING\n" + frag,
uniforms: {
pickingColor: {
value: new THREE.Color(),
},
},
});
var matrix = new THREE.Matrix4();
for (var i = 0; i < instanceCount; i++) {
var object = new THREE.Mesh(geo, material);
objectCount++;
randomizeMatrix(matrix);
object.applyMatrix(matrix);
var pickingObject = object.clone();
objectCount++;
object.material = material.clone();
object.material.uniforms.color.value.setHex(Math.random() * 0xffffff);
materialList.push(object.material);
pickingObject.material = pickingMaterial.clone();
pickingObject.material.uniforms.pickingColor.value.setHex(i + 1);
materialList.push(pickingObject.material);
pickingData[i + 1] = object;
scene.add(object);
pickingScene.add(pickingObject);
}
material.dispose();
pickingMaterial.dispose();
}
function makeSingleMaterial(geo) {
var vert = document.getElementById("vertMaterial").textContent;
var frag = document.getElementById("fragMaterial").textContent;
var material = new THREE.RawShaderMaterial({
vertexShader: vert,
fragmentShader: frag,
uniforms: {
color: {
value: new THREE.Color(),
},
},
});
materialList.push(material);
var pickingMaterial = new THREE.RawShaderMaterial({
vertexShader: "#define PICKING\n" + vert,
fragmentShader: "#define PICKING\n" + frag,
uniforms: {
pickingColor: {
value: new THREE.Color(),
},
},
});
materialList.push(pickingMaterial);
if (useOverrideMaterial) {
singleMaterial = material;
singlePickingMaterial = pickingMaterial;
}
var matrix = new THREE.Matrix4();
function onBeforeRender(
renderer,
scene,
camera,
geometry,
material,
group
) {
var updateList = [];
var u = material.uniforms;
var d = this.userData;
if (u.pickingColor) {
u.pickingColor.value.setHex(d.pickingColor);
updateList.push("pickingColor");
}
if (u.color) {
u.color.value.setHex(d.color);
updateList.push("color");
}
if (updateList.length) {
var materialProperties = renderer.properties.get(material);
if (materialProperties.program) {
var gl = renderer.getContext();
var p = materialProperties.program;
gl.useProgram(p.program);
var pu = p.getUniforms();
updateList.forEach(function (name) {
pu.setValue(gl, name, u[name].value);
});
}
}
}
for (var i = 0; i < instanceCount; i++) {
var object = new THREE.Mesh(geo, material);
objectCount++;
randomizeMatrix(matrix);
object.applyMatrix(matrix);
var pickingObject;
if (!useOverrideMaterial) {
pickingObject = object.clone();
objectCount++;
}
object.material = material;
object.userData["color"] = Math.random() * 0xffffff;
if (useOverrideMaterial) {
object.userData["pickingColor"] = i + 1;
object.onBeforeRender = onBeforeRender;
} else {
pickingObject.material = pickingMaterial;
pickingObject.userData["pickingColor"] = i + 1;
pickingObject.onBeforeRender = onBeforeRender;
}
pickingData[i + 1] = object;
scene.add(object);
if (!useOverrideMaterial) pickingScene.add(pickingObject);
}
}
function makeMerged(geo) {
var vert = document.getElementById("vertMerged").textContent;
var frag = document.getElementById("fragMerged").textContent;
var material = new THREE.RawShaderMaterial({
vertexShader: vert,
fragmentShader: frag,
});
materialList.push(material);
var pickingMaterial = new THREE.RawShaderMaterial({
vertexShader: "#define PICKING\n" + vert,
fragmentShader: "#define PICKING\n" + frag,
});
materialList.push(pickingMaterial);
var bgeo = geo.clone();
geometryList.push(bgeo);
var mgeo = new THREE.BufferGeometry();
geometryList.push(mgeo);
var pos = bgeo.attributes.position;
var posLen = bgeo.attributes.position.count * 3;
var vertices = new THREE.BufferAttribute(
new Float32Array(instanceCount * posLen),
3
);
var matrix = new THREE.Matrix4();
for (var i = 0, ul = instanceCount; i < ul; i++) {
randomizeMatrix(matrix);
var object = new THREE.Object3D();
objectCount++;
object.applyMatrix(matrix);
pickingData[i + 1] = object;
vertices.set(pos.array, i * posLen);
//matrix.applyToVector3Array( vertices.array, i * posLen, posLen )
}
mgeo.addAttribute("position", vertices);
var colCount = posLen / 3;
var colors = new THREE.BufferAttribute(
new Float32Array(instanceCount * colCount * 3),
3
);
var randCol = function () {
return Math.random();
};
for (var i = 0, ul = instanceCount; i < ul; i++) {
var r = randCol(),
g = randCol(),
b = randCol();
for (var j = i * colCount, jl = (i + 1) * colCount; j < jl; j++) {
colors.setXYZ(j, r, g, b);
}
}
mgeo.addAttribute("color", colors);
var col = new THREE.Color();
var pickingColors = new THREE.BufferAttribute(
new Float32Array(instanceCount * colCount * 3),
3
);
for (var i = 0, ul = instanceCount; i < ul; i++) {
col.setHex(i + 1);
for (var j = i * colCount, jl = (i + 1) * colCount; j < jl; j++) {
pickingColors.setXYZ(j, col.r, col.g, col.b);
}
}
mgeo.addAttribute("pickingColor", pickingColors);
var mesh = new THREE.Mesh(mgeo, material);
scene.add(mesh);
var pickingMesh = new THREE.Mesh(mgeo, pickingMaterial);
pickingScene.add(pickingMesh);
}
function makeInstanced(geo) {
var vert = document.getElementById("vertInstanced").textContent;
var frag = document.getElementById("fragInstanced").textContent;
var material = new THREE.RawShaderMaterial({
vertexShader: vert,
fragmentShader: frag,
});
materialList.push(material);
var pickingMaterial = new THREE.RawShaderMaterial({
vertexShader: "#define PICKING\n" + vert,
fragmentShader: "#define PICKING\n" + frag,
});
materialList.push(pickingMaterial);
var bgeo = geo.clone();
geometryList.push(bgeo);
var igeo = new THREE.InstancedBufferGeometry();
geometryList.push(igeo);
var vertices = bgeo.attributes.position.clone();
igeo.addAttribute("position", vertices);
var mcol0 = new THREE.InstancedBufferAttribute(
new Float32Array(instanceCount * 3),
3,
1
);
var mcol1 = new THREE.InstancedBufferAttribute(
new Float32Array(instanceCount * 3),
3,
1
);
var mcol2 = new THREE.InstancedBufferAttribute(
new Float32Array(instanceCount * 3),
3,
1
);
var mcol3 = new THREE.InstancedBufferAttribute(
new Float32Array(instanceCount * 3),
3,
1
);
var matrix = new THREE.Matrix4();
var me = matrix.elements;
for (var i = 0, ul = mcol0.count; i < ul; i++) {
randomizeMatrix(matrix);
var object = new THREE.Object3D();
objectCount++;
object.applyMatrix(matrix);
pickingData[i + 1] = object;
mcol0.setXYZ(i, me[0], me[1], me[2]);
mcol1.setXYZ(i, me[4], me[5], me[6]);
mcol2.setXYZ(i, me[8], me[9], me[10]);
mcol3.setXYZ(i, me[12], me[13], me[14]);
}
igeo.addAttribute("mcol0", mcol0);
igeo.addAttribute("mcol1", mcol1);
igeo.addAttribute("mcol2", mcol2);
igeo.addAttribute("mcol3", mcol3);
var randCol = function () {
return Math.random();
};
var colors = new THREE.InstancedBufferAttribute(
new Float32Array(instanceCount * 3),
3,
1
);
for (var i = 0, ul = colors.count; i < ul; i++) {
colors.setXYZ(i, randCol(), randCol(), randCol());
}
igeo.addAttribute("color", colors);
var col = new THREE.Color();
var pickingColors = new THREE.InstancedBufferAttribute(
new Float32Array(instanceCount * 3),
3,
1
);
for (var i = 0, ul = pickingColors.count; i < ul; i++) {
col.setHex(i + 1);
pickingColors.setXYZ(i, col.r, col.g, col.b);
}
igeo.addAttribute("pickingColor", pickingColors);
var mesh = new THREE.Mesh(igeo, material);
scene.add(mesh);
var pickingMesh = new THREE.Mesh(igeo, pickingMaterial);
pickingScene.add(pickingMesh);
}
function init() {
camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
1,
100
);
camera.position.z = 40;
pickingRenderTarget = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight
);
pickingRenderTarget.texture.generateMipmaps = false;
pickingRenderTarget.texture.minFilter = THREE.NearestFilter;
highlightBox = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshLambertMaterial({
emissive: 0xffff00,
transparent: true,
opacity: 0.5,
side: THREE.FrontSide,
})
);
container = document.getElementById("container");
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
if (renderer.extensions.get("ANGLE_instanced_arrays") === false) {
document.getElementById("notSupported").style.display = "";
return;
}
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
if (renderer.extensions.get("ANGLE_instanced_arrays") === false) {
throw "ANGLE_instanced_arrays not supported";
}
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.staticMoving = true;
stats = new Stats();
container.appendChild(stats.dom);
renderer.domElement.addEventListener("mousemove", onMouseMove);
window.addEventListener("resize", onWindowResize, false);
}
//
function onMouseMove(e) {
mouse.x = e.clientX;
mouse.y = e.clientY;
controls.update();
requestAnimationFrame(render);
}
function onWindowResize(event) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
pickingRenderTarget.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
if (doAnimate) {
requestAnimationFrame(animate);
}
controls.update();
stats.update();
document.getElementById("materialCount").innerText =
materialList.length;
document.getElementById("objectCount").innerText = objectCount;
document.getElementById("drawcalls").innerText =
renderer.info.render.calls;
render();
}
function pick() {
highlightBox.visible = false;
if (singlePickingMaterial) {
scene.overrideMaterial = singlePickingMaterial;
renderer.render(scene, camera, pickingRenderTarget);
scene.overrideMaterial = null;
} else {
renderer.render(pickingScene, camera, pickingRenderTarget);
}
renderer.readRenderTargetPixels(
pickingRenderTarget,
mouse.x,
pickingRenderTarget.height - mouse.y,
1,
1,
pixelBuffer
);
var id =
(pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2];
var object = pickingData[id];
if (object) {
if (object.position && object.rotation && object.scale) {
highlightBox.position.copy(object.position);
highlightBox.rotation.copy(object.rotation);
highlightBox.scale
.copy(object.scale)
.multiply(geometrySize)
.multiplyScalar(scale);
highlightBox.visible = true;
}
} else {
highlightBox.visible = false;
}
}
function render() {
pick();
renderer.render(scene, camera);
}
</script>
</body>
</html>
It seems like your question is primarily, how to do instancing in three.js? Once you've loaded a model, it doesn't really matter what format you used to create it.
That being the case, you probably just want to check out the three.js instancing examples or use one of the helpers like three-instanced-mesh.
The second link shows how to proceed, once you've grabbed the geometry from your model:
// Assumes your model only contains one mesh.
var geometry;
model.traverse(function (node) => {
if (node.isMesh) {
geometry = node.geometry;
}
});
//material that the geometry will use
var material = new THREE.MeshPhongMaterial();
//the instance group
var cluster = new THREE.InstancedMesh(
geometry,
material,
10000, //instance count
false, //is it dynamic
false //does it have color
true, //uniform scale
);
var _v3 = new THREE.Vector3();
var _q = new THREE.Quaternion();
for ( var i ; i < 10000 ; i ++ ) {
cluster.setQuaternionAt( i , _q );
cluster.setPositionAt( i , v3.set( Math.random() , Math.random(), Math.random() ) );
cluster.setScaleAt( i , v3.set(1,1,1) );
}
scene.add( cluster );
After some time of investigation I discovered why using instancedbuffergeometries were not working with the buffergeometries found in my GLTF files.
The problem is that GLTF format uses indexedbuffergeometries and the workaround is very simple, just convert them with toNonIndexed() method.
Issue fixed.
Best regards
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With