在Three.js中成功渲染数万个大小/颜色/位置可变的球体



这个问题源于我上一个问题,我发现使用Points会导致问题:https://stackoverflow.com/a/60306638/4749956

要解决此问题,您需要使用四边形而不是点来绘制点。有很多方法可以做到这一点。将每个四边形绘制为单独的网格或精灵,或将所有四边形合并到另一个网格中,或使用InstancedMesh,其中每个点需要一个矩阵,或编写自定义着色器来点(请参阅本文的最后一个示例)

我一直在努力找出这个答案。我的问题是

什么是"实例化"?合并几何图形和实例化之间的区别是什么?如果我要做其中任何一个,我会使用什么几何图形,以及如何改变颜色?我一直在看这个例子:

https://github.com/mrdoob/three.js/blob/master/examples/webgl_instancing_performance.html

我看到,对于每个球体,你都会有一个几何体,它会应用位置和大小(比例?)。那么,基础几何体会是单位半径的SphereBufferGeometry吗?但是,你是如何应用颜色的?

此外,我读过关于自定义着色器方法的文章,它有一些模糊的意义。但是,这似乎更为复杂。性能会比上面的好吗?

基于您以前的问题。。。

首先,实例化是一种告诉三个.js多次绘制同一个几何体,但为每个"实例"多更改一个内容的方法。IIRC three.js唯一支持的开箱即用就是为每个实例设置不同的矩阵(位置、方向、比例)。在此之后,例如,就像使用不同的颜色一样,您必须编写自定义着色器。

实例化允许你要求系统用一个"问"而不是每件事一个"求"来画很多东西。这意味着它最终会更快。你可以把它想象成任何东西。如果想要3个汉堡,你可以请人给你做1个。当他们完成后,你可以要求他们再做一个。当他们完成时,你可以要求他们做第三个。这比一开始就要求他们制作3个汉堡要慢得多。这不是一个完美的类比,但它确实指出了一次要求一件多件事比一次要求多件事效率低。

合并网格是另一种解决方案,按照上面的类比,合并网格就像制作一个1磅重的大汉堡,而不是三个1/3磅重的汉堡。翻转一个大汉堡,在一个大的汉堡上放上浇头和面包,比对三个小汉堡做同样的动作要快一点。

至于哪种解决方案对你来说是最好的,这取决于你。在最初的代码中,您只是使用"点"绘制有纹理的四边形。点总是在屏幕空间中绘制四边形。另一方面,默认情况下,网格在世界空间中旋转,因此如果您创建四元体或合并的四元体集的实例并尝试旋转它们,它们将像点那样旋转而不面向摄影机。如果您使用球体几何体,则会遇到这样的问题,即每个四元体上绘制一个圆,而不是只计算6个顶点,你将计算每个球体100或1000个顶点,这将比每个四边形6个顶点慢。

因此,它再次需要一个自定义着色器来保持点面向摄影机。

要对短版本进行实例化,需要决定每个实例重复哪些顶点数据。例如,对于纹理四边形,我们需要6个顶点位置和6个uvs。对于这些,您可以制作正常的BufferAttribute

然后决定哪些顶点数据对每个实例都是唯一的。在您的情况下,大小、颜色和点的中心。对于其中的每一个,我们都制作了一个InstancedBufferAttribute

我们将所有这些属性添加到InstancedBufferGeometry中,作为最后一个参数,我们告诉它有多少实例。

在抽签时,你可以把它想象成这个

每个实例的
    • 将size设置为size属性中的下一个值
    • 将颜色设置为颜色属性中的下一个值
    • 将center设置为center属性中的下一个值
    • 调用顶点着色器6次,位置和uv在其属性中设置为第n个值

通过这种方式,您可以多次使用相同的几何体(位置和uvs),但每次都会更改一些值(大小、颜色、中心)。

body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
#info {
position: absolute;
right: 0;
bottom: 0;
color: red;
background: black;
}
<canvas id="c"></canvas>
<div id="info"></div>
<script type="module">
// Three.js - Picking - RayCaster w/Transparency
// from https://threejsfundamentals.org/threejs/threejs-picking-gpu.html
import * as THREE from "https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.module.js";
function main() {
const infoElem = document.querySelector("#info");
const canvas = document.querySelector("#c");
const renderer = new THREE.WebGLRenderer({ canvas });
const fov = 60;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 30;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0);
const pickingScene = new THREE.Scene();
pickingScene.background = new THREE.Color(0);
// put the camera on a pole (parent it to an object)
// so we can spin the pole to move the camera around the scene
const cameraPole = new THREE.Object3D();
scene.add(cameraPole);
cameraPole.add(camera);
function randomNormalizedColor() {
return Math.random();
}
function getRandomInt(n) {
return Math.floor(Math.random() * n);
}
function getCanvasRelativePosition(e) {
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
const textureLoader = new THREE.TextureLoader();
const particleTexture =
"https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/sprites/ball.png";
const vertexShader = `
attribute float size;
attribute vec3 customColor;
attribute vec3 center;
varying vec3 vColor;
varying vec2 vUv;
void main() {
vColor = customColor;
vUv = uv;
vec3 viewOffset = position * size ;
vec4 mvPosition = modelViewMatrix * vec4(center, 1) + vec4(viewOffset, 0);
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
uniform sampler2D texture;
varying vec3 vColor;
varying vec2 vUv;
void main() {
vec4 tColor = texture2D(texture, vUv);
if (tColor.a < 0.5) discard;
gl_FragColor = mix(vec4(vColor.rgb, 1.0), tColor, 0.1);
}
`;
const pickFragmentShader = `
uniform sampler2D texture;
varying vec3 vColor;
varying vec2 vUv;
void main() {
vec4 tColor = texture2D(texture, vUv);
if (tColor.a < 0.25) discard;
gl_FragColor = vec4(vColor.rgb, 1.0);
}
`;
const materialSettings = {
uniforms: {
texture: {
type: "t",
value: textureLoader.load(particleTexture)
}
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
blending: THREE.NormalBlending,
depthTest: true,
transparent: false
};
const createParticleMaterial = () => {
const material = new THREE.ShaderMaterial(materialSettings);
return material;
};
const createPickingMaterial = () => {
const material = new THREE.ShaderMaterial({
...materialSettings,
fragmentShader: pickFragmentShader,
blending: THREE.NormalBlending
});
return material;
};
const geometry = new THREE.InstancedBufferGeometry();
const pickingGeometry = new THREE.InstancedBufferGeometry();
const colors = [];
const sizes = [];
const pickingColors = [];
const pickingColor = new THREE.Color();
const centers = [];
const numSpheres = 30;
const positions = [
-0.5, -0.5,
0.5, -0.5,
-0.5,  0.5,
-0.5,  0.5,
0.5, -0.5,
0.5,  0.5,
];
const uvs = [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
];
for (let i = 0; i < numSpheres; i++) {
colors[3 * i] = randomNormalizedColor();
colors[3 * i + 1] = randomNormalizedColor();
colors[3 * i + 2] = randomNormalizedColor();
const rgbPickingColor = pickingColor.setHex(i + 1);
pickingColors[3 * i] = rgbPickingColor.r;
pickingColors[3 * i + 1] = rgbPickingColor.g;
pickingColors[3 * i + 2] = rgbPickingColor.b;
sizes[i] = getRandomInt(5);
centers[3 * i] = getRandomInt(20);
centers[3 * i + 1] = getRandomInt(20);
centers[3 * i + 2] = getRandomInt(20);
}
geometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(positions, 2)
);
geometry.setAttribute(
"uv",
new THREE.Float32BufferAttribute(uvs, 2)
);
geometry.setAttribute(
"customColor",
new THREE.InstancedBufferAttribute(new Float32Array(colors), 3)
);
geometry.setAttribute(
"center",
new THREE.InstancedBufferAttribute(new Float32Array(centers), 3)
);
geometry.setAttribute(
"size",
new THREE.InstancedBufferAttribute(new Float32Array(sizes), 1));
const material = createParticleMaterial();
const points = new THREE.InstancedMesh(geometry, material, numSpheres);
// setup geometry and material for GPU picking
pickingGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(positions, 2)
);
pickingGeometry.setAttribute(
"uv",
new THREE.Float32BufferAttribute(uvs, 2)
);
pickingGeometry.setAttribute(
"customColor",
new THREE.InstancedBufferAttribute(new Float32Array(pickingColors), 3)
);
pickingGeometry.setAttribute(
"center",
new THREE.InstancedBufferAttribute(new Float32Array(centers), 3)
);
pickingGeometry.setAttribute(
"size",
new THREE.InstancedBufferAttribute(new Float32Array(sizes), 1)
);
const pickingMaterial = createPickingMaterial();
const pickingPoints = new THREE.InstancedMesh(pickingGeometry, pickingMaterial, numSpheres);
scene.add(points);
pickingScene.add(pickingPoints);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
class GPUPickHelper {
constructor() {
// create a 1x1 pixel render target
this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
this.pixelBuffer = new Uint8Array(4);
}
pick(cssPosition, pickingScene, camera) {
const { pickingTexture, pixelBuffer } = this;
// set the view offset to represent just a single pixel under the mouse
const pixelRatio = renderer.getPixelRatio();
camera.setViewOffset(
renderer.getContext().drawingBufferWidth, // full width
renderer.getContext().drawingBufferHeight, // full top
(cssPosition.x * pixelRatio) | 0, // rect x
(cssPosition.y * pixelRatio) | 0, // rect y
1, // rect width
1 // rect height
);
// render the scene
renderer.setRenderTarget(pickingTexture);
renderer.render(pickingScene, camera);
renderer.setRenderTarget(null);
// clear the view offset so rendering returns to normal
camera.clearViewOffset();
//read the pixel
renderer.readRenderTargetPixels(
pickingTexture,
0, // x
0, // y
1, // width
1, // height
pixelBuffer
);
const id =
(pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | pixelBuffer[2];
infoElem.textContent = `You clicked sphere number ${id}`;
return id;
}
}
const pickHelper = new GPUPickHelper();
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cameraPole.rotation.y = time * 0.1;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function onClick(e) {
const pickPosition = getCanvasRelativePosition(e);
const pickedID = pickHelper.pick(pickPosition, pickingScene, camera);
}
function onTouch(e) {
const touch = e.touches[0];
const pickPosition = getCanvasRelativePosition(touch);
const pickedID = pickHelper.pick(pickPosition, pickingScene, camera);
}
window.addEventListener("mousedown", onClick);
window.addEventListener("touchstart", onTouch);
}
main();
</script>

这是一个相当宽泛的主题。简言之,合并和实例化都是为了减少渲染时的绘制调用次数。

如果你绑定一次球体几何体,但不断重新渲染它,那么告诉你的计算机多次绘制它的成本要比你的计算机计算绘制它所需的成本高。你最终会发现GPU,一个强大的并行处理设备,闲置着。

显然,如果你在空间中的每个点创建一个唯一的球体,并将它们全部合并,你就要付出让gpu渲染一次的代价,它将忙于渲染数千个球体。

然而,合并会增加内存占用,并且在实际创建唯一数据时会产生一些开销。实例化是一种内置的聪明方法,可以以更低的内存成本实现同样的效果。

我有一篇关于这个主题的文章。

相关内容

  • 没有找到相关文章

最新更新