如何使用Three.js InstancedBufferGeometry & InstancedBufferAttribute?
How to use Three.js InstancedBufferGeometry & InstancedBufferAttribute?
我相信 "instancing" 如此处所述提供了一种为所有 vertices/indicies 提供一个属性的方法,例如 200 个顶点模型:
换句话说,这提供了一种只有一个平移或方向属性数组的方法,该数组将应用于模型的所有 200 个顶点。因此 "instancing" 这些模型的 10K 场景只需要 10K 属性,而不是 2000K。
显然 Three 的 InstancedBufferGeometry 和 InstancedBufferAttribute 对象提供了这个,但我没有找到文档,除了对象的稀疏描述。我相信他们使用 ShaderMaterial,这很好,尽管在 "vanilla" Three.js.
中可能还有其他方法而不是使用 GLSL
有人可以解释一下它们的工作原理以及如何在 Three.js 中使用它们吗?
问题有点绕,我会尽力的。
我认为您混淆了一些事情,我在 three.js 用户中经常看到这种情况。首先,我认为属性术语是错误的。您不是在创建成千上万的属性,而是像网格这样的实体可能具有一个属性 - position
,或多个 uv
、normal
、aMyAttribute
等.
事实上,webgl 可以处理的属性数量是有上限的,这有所不同,但大概是 16 个,而不是数千个。
一个属性可以包含定义200个的数据"vertices"但这也是相对的,它们可以有2个分量,也可以有4个分量。
当您从“200 顶点模型”中创建多个 "objects" 时,您并不是在乘以几何体。属性计数没有增加,我实际上不确定制服会发生什么,但就 gpu 而言,它仍然拥有 200 个顶点。 Javascript 仍然包含一些属性和统一位置,以及一些 Geometry
class 的一个实例。
你所做的乘法是"nodes/objects",在javascript你将有多个说Mesh
对象。这将包含各种其他类型、矩阵、向量、四元数等。
当您调用渲染函数时,渲染器将为这些节点中的每一个发出绘制调用。每次这样做时,它都需要设置 webgl 的状态来处理那个特定的绘图调用。如果这是散布在场景周围的同一对象,唯一不同的是 position/rotation/scale 的制服。如果他们有材料,那么它可能是颜色的统一,或不同的纹理。无论哪种方式,这都会产生开销,并减慢速度。
假设您的模型是一棵树。从中创建一个森林并渲染 a forest
,而不是 many trees
将消除这种开销,仍然有相同数量的着色器处理正在进行。每棵树的每个顶点都需要有一个着色器运行。
这当然会增加您的属性持有的数据。现在,在一些方便的对象 space(最好称它为 "tree space" )中,它不需要包含 200 个顶点,而是需要在 "forest space" 中包含 200 x N 个顶点。 IE。 tree 0
的顶点存在于森林中的某处,而 tree N
的相同顶点存在于森林中的其他地方。如果您要为每棵树创建一个新几何体,将变换烘焙到它的顶点,然后与另一棵树合并等等,就会发生这种情况。
实例化可以让你更聪明地处理这种情况。您可以保存原始树的 200 个顶点,以及一个描述它们将在何处绘制 N 次的属性,而不是保存共享一个公共 属性(它是同一棵树)的所有那些单独的顶点。因此,与其合并几何图形并单独对它们烘焙变换,不如构造一个仅包含变换的属性。
您很可能无法将其与原版材料一起使用,因为它们不知道如何处理您的自定义属性。但是,根据 three.js 处理着色器的方式,注入一些逻辑并扩展现有材质并不难。
我自己寻找这个答案时偶然发现了你的问题。这里只是两个使用实例化的示例(直接来自 threejs.org/examples):
- https://threejs.org/examples/?q=instanc#webgl_buffergeometry_instancing
- https://threejs.org/examples/?q=instanc#webgl_buffergeometry_instancing_dynamic
简单说明:
THREE.InstancedBufferGeometry
和 THREE.BufferGeometry
之间的主要区别是前者可以使用特殊属性 (THREE.InstancedBufferAttributes
) 每个实例[=56] =].
假设您正在创建一个盒子,您希望它有多个实例。顶点、法线和 UV 缓冲区都是标准 THREE.BufferAttribute
对象,因为它们描述了基本形状。但是为了将每个实例移动到它自己的位置,您需要定义一个 THREE.InstancedBufferAttribute
来保存位置(示例通常将此属性命名为“offset
”)。
您 THREE.InstancedBufferAttributes
中的顶点引用数描述了您将拥有的实例数。例如,将 9 个值放入 offset
表示将有 3 个实例(这包括原始形状)。您还可以通过设置 THREE.InstancedBuferGeometry.maxInstancedCount
值来控制绘制的数量。
最后,您将需要一个着色器来帮助控制实例化属性。
小例子:
var cubeGeo = new THREE.InstancedBufferGeometry().copy(new THREE.BoxBufferGeometry(10, 10, 10));
//cubeGeo.maxInstancedCount = 8;
cubeGeo.addAttribute("cubePos", new THREE.InstancedBufferAttribute(new Float32Array([
25, 25, 25,
25, 25, -25, -25, 25, 25, -25, 25, -25,
25, -25, 25,
25, -25, -25, -25, -25, 25, -25, -25, -25
]), 3, 1));
var vertexShader = [
"precision highp float;",
"",
"uniform mat4 modelViewMatrix;",
"uniform mat4 projectionMatrix;",
"",
"attribute vec3 position;",
"attribute vec3 cubePos;",
"",
"void main() {",
"",
" gl_Position = projectionMatrix * modelViewMatrix * vec4( cubePos + position, 1.0 );",
"",
"}"
].join("\n");
var fragmentShader = [
"precision highp float;",
"",
"void main() {",
"",
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);",
"",
"}"
].join("\n");
var mat = new THREE.RawShaderMaterial({
uniforms: {},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
transparent: false
});
var mesh = new THREE.Mesh(cubeGeo, mat);
scene.add(mesh);
html * {
padding: 0;
margin: 0;
width: 100%;
overflow: hidden;
}
#host {
width: 100%;
height: 100%;
}
<script src="https://threejs.org/build/three.js"></script>
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
<script src="https://threejs.org/examples/js/libs/stats.min.js"></script>
<div id="host"></div>
<script>
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 35,
NEAR = 1,
FAR = 1000;
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(WIDTH, HEIGHT);
document.getElementById('host').appendChild(renderer.domElement);
var stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
var camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 250;
var trackballControl = new THREE.TrackballControls(camera, renderer.domElement);
trackballControl.rotateSpeed = 5.0; // need to speed it up a little
var scene = new THREE.Scene();
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
scene.add(light);
function render() {
if (typeof updateVertices !== "undefined") {
updateVertices();
}
renderer.render(scene, camera);
stats.update();
}
function animate() {
requestAnimationFrame(animate);
trackballControl.update();
render();
}
animate();
</script>
我相信 "instancing" 如此处所述提供了一种为所有 vertices/indicies 提供一个属性的方法,例如 200 个顶点模型:
换句话说,这提供了一种只有一个平移或方向属性数组的方法,该数组将应用于模型的所有 200 个顶点。因此 "instancing" 这些模型的 10K 场景只需要 10K 属性,而不是 2000K。
显然 Three 的 InstancedBufferGeometry 和 InstancedBufferAttribute 对象提供了这个,但我没有找到文档,除了对象的稀疏描述。我相信他们使用 ShaderMaterial,这很好,尽管在 "vanilla" Three.js.
中可能还有其他方法而不是使用 GLSL有人可以解释一下它们的工作原理以及如何在 Three.js 中使用它们吗?
问题有点绕,我会尽力的。
我认为您混淆了一些事情,我在 three.js 用户中经常看到这种情况。首先,我认为属性术语是错误的。您不是在创建成千上万的属性,而是像网格这样的实体可能具有一个属性 - position
,或多个 uv
、normal
、aMyAttribute
等.
事实上,webgl 可以处理的属性数量是有上限的,这有所不同,但大概是 16 个,而不是数千个。
一个属性可以包含定义200个的数据"vertices"但这也是相对的,它们可以有2个分量,也可以有4个分量。
当您从“200 顶点模型”中创建多个 "objects" 时,您并不是在乘以几何体。属性计数没有增加,我实际上不确定制服会发生什么,但就 gpu 而言,它仍然拥有 200 个顶点。 Javascript 仍然包含一些属性和统一位置,以及一些 Geometry
class 的一个实例。
你所做的乘法是"nodes/objects",在javascript你将有多个说Mesh
对象。这将包含各种其他类型、矩阵、向量、四元数等。
当您调用渲染函数时,渲染器将为这些节点中的每一个发出绘制调用。每次这样做时,它都需要设置 webgl 的状态来处理那个特定的绘图调用。如果这是散布在场景周围的同一对象,唯一不同的是 position/rotation/scale 的制服。如果他们有材料,那么它可能是颜色的统一,或不同的纹理。无论哪种方式,这都会产生开销,并减慢速度。
假设您的模型是一棵树。从中创建一个森林并渲染 a forest
,而不是 many trees
将消除这种开销,仍然有相同数量的着色器处理正在进行。每棵树的每个顶点都需要有一个着色器运行。
这当然会增加您的属性持有的数据。现在,在一些方便的对象 space(最好称它为 "tree space" )中,它不需要包含 200 个顶点,而是需要在 "forest space" 中包含 200 x N 个顶点。 IE。 tree 0
的顶点存在于森林中的某处,而 tree N
的相同顶点存在于森林中的其他地方。如果您要为每棵树创建一个新几何体,将变换烘焙到它的顶点,然后与另一棵树合并等等,就会发生这种情况。
实例化可以让你更聪明地处理这种情况。您可以保存原始树的 200 个顶点,以及一个描述它们将在何处绘制 N 次的属性,而不是保存共享一个公共 属性(它是同一棵树)的所有那些单独的顶点。因此,与其合并几何图形并单独对它们烘焙变换,不如构造一个仅包含变换的属性。
您很可能无法将其与原版材料一起使用,因为它们不知道如何处理您的自定义属性。但是,根据 three.js 处理着色器的方式,注入一些逻辑并扩展现有材质并不难。
我自己寻找这个答案时偶然发现了你的问题。这里只是两个使用实例化的示例(直接来自 threejs.org/examples):
- https://threejs.org/examples/?q=instanc#webgl_buffergeometry_instancing
- https://threejs.org/examples/?q=instanc#webgl_buffergeometry_instancing_dynamic
简单说明:
THREE.InstancedBufferGeometry
和 THREE.BufferGeometry
之间的主要区别是前者可以使用特殊属性 (THREE.InstancedBufferAttributes
) 每个实例[=56] =].
假设您正在创建一个盒子,您希望它有多个实例。顶点、法线和 UV 缓冲区都是标准 THREE.BufferAttribute
对象,因为它们描述了基本形状。但是为了将每个实例移动到它自己的位置,您需要定义一个 THREE.InstancedBufferAttribute
来保存位置(示例通常将此属性命名为“offset
”)。
您 THREE.InstancedBufferAttributes
中的顶点引用数描述了您将拥有的实例数。例如,将 9 个值放入 offset
表示将有 3 个实例(这包括原始形状)。您还可以通过设置 THREE.InstancedBuferGeometry.maxInstancedCount
值来控制绘制的数量。
最后,您将需要一个着色器来帮助控制实例化属性。
小例子:
var cubeGeo = new THREE.InstancedBufferGeometry().copy(new THREE.BoxBufferGeometry(10, 10, 10));
//cubeGeo.maxInstancedCount = 8;
cubeGeo.addAttribute("cubePos", new THREE.InstancedBufferAttribute(new Float32Array([
25, 25, 25,
25, 25, -25, -25, 25, 25, -25, 25, -25,
25, -25, 25,
25, -25, -25, -25, -25, 25, -25, -25, -25
]), 3, 1));
var vertexShader = [
"precision highp float;",
"",
"uniform mat4 modelViewMatrix;",
"uniform mat4 projectionMatrix;",
"",
"attribute vec3 position;",
"attribute vec3 cubePos;",
"",
"void main() {",
"",
" gl_Position = projectionMatrix * modelViewMatrix * vec4( cubePos + position, 1.0 );",
"",
"}"
].join("\n");
var fragmentShader = [
"precision highp float;",
"",
"void main() {",
"",
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);",
"",
"}"
].join("\n");
var mat = new THREE.RawShaderMaterial({
uniforms: {},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
transparent: false
});
var mesh = new THREE.Mesh(cubeGeo, mat);
scene.add(mesh);
html * {
padding: 0;
margin: 0;
width: 100%;
overflow: hidden;
}
#host {
width: 100%;
height: 100%;
}
<script src="https://threejs.org/build/three.js"></script>
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
<script src="https://threejs.org/examples/js/libs/stats.min.js"></script>
<div id="host"></div>
<script>
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 35,
NEAR = 1,
FAR = 1000;
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(WIDTH, HEIGHT);
document.getElementById('host').appendChild(renderer.domElement);
var stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
var camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 250;
var trackballControl = new THREE.TrackballControls(camera, renderer.domElement);
trackballControl.rotateSpeed = 5.0; // need to speed it up a little
var scene = new THREE.Scene();
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
scene.add(light);
function render() {
if (typeof updateVertices !== "undefined") {
updateVertices();
}
renderer.render(scene, camera);
stats.update();
}
function animate() {
requestAnimationFrame(animate);
trackballControl.update();
render();
}
animate();
</script>