ThreeJS學習6_幾何體相關(BufferGeometry)

隨遇丿而安發表於2020-10-20

ThreeJS學習6_幾何體相關(BufferGeometry)


使用 BufferGeometry 可以有效減少向 GPU 傳輸幾何體相關資料所需的開銷

可以自定義頂點位置, 面片索引, 法向量, 顏色值

1. BufferGeometry使用初體驗

在之前的學習中, 我是已經瞭解到建立一個3d場景, 不知道螢幕前的你是否有了解到, threejs需要做的有, 第一: 渲染器renderer; 第二: 場景Scene; 第三, 光源Light; 第四, 物質有點線面三個部分.

在實際的開發過程中, 自己建立幾何體這種情況很少見, 大部分情況是載入已有的模型, 對模型進行操作, 匯入的模型可能很大, 這個時候就需要優化, 優化可以從幾何體入手, 也可以從材質入手, 但是優化主要針對的就是幾何體, 佔記憶體的也是幾何體而不是材質, 因此瞭解幾何體是非常有必要的, 幾何體英文( geometry ).

對於Threejs, 官方說明, 使用buffergeometry能夠有效減少向GPU傳輸幾何體相關資料所需要的開銷, 同時, 使用者可以自定義集合體的頂點位置, 名片索引, 法向量, 顏色值

下面建立一個簡單的buffergeometry吧

// 頂點個數
var particles = 500000;

var geometry = new THREE.BufferGeometry();

// 每個頂點位置
let positions = [];
// 顏色值
var colors = [];

// 臨時顏色型別
var color = new THREE.Color();

var n = 1000, n2 = n / 2; 

for ( var i = 0; i < particles; i ++ ) {

 // positions, 形成一個長方體, x, y, z的範圍都是從-500到500, 形成的長方體的長寬高都為500
 var x = Math.random() * n - n2;
 var y = Math.random() * n - n2;
 var z = Math.random() * n - n2;

 positions.push( x, y, z );

 // colors, 設定顏色, 同理, 從0到1

 var vx = ( x / n ) + 0.5;
 var vy = ( y / n ) + 0.5;
 var vz = ( z / n ) + 0.5;

 color.setRGB( vx, vy, vz );

 colors.push( color.r, color.g, color.b );

}
// 設定位置資訊
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
// 設定顏色資訊
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
// 計算邊界球體
geometry.computeBoundingSphere();

// 建立物資
var material = new THREE.PointsMaterial( { size: 15, vertexColors: true } );
// 建立點雲
points = new THREE.Points( geometry, material );
scene.add( points );

效果如下圖所示

簡單示意圖

下面對程式碼進行簡單的分析, 並進行彙總

程式碼主要分為三步

  1. 建立所有點的位置陣列, 每三個值形成x, y, z確定三維世界點的座標
    • 對應positions = [],
    • positions.push()
  2. 建立所有點的顏色陣列, 每三個值形成r, g, b確定三維世界點的顏色
    • colors = []
    • colors.push()
  3. 將位置陣列和顏色陣列匯入到集合體中
    • geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
    • geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );

根據程式碼, 將建好的點雲加入場景中, 就有效果了, 完整程式碼附在文章末尾處

2. 簡單壓縮幾何體的方法

threejs給我們提供了一些可以直接引用的方法降低GPU渲染幾何體的開銷, 這裡展示官方給的3種型別的程式碼

裡面第一行程式碼是用於計算mesh在GPU中所佔記憶體

// 計算這個mesh在gpu中所佔記憶體
BufferGeometryUtils.estimateBytesUsed( mesh.geometry ) + " bytes"

// 使用DefaultUVEncoding降低記憶體數
GeometryCompressionUtils.compressUvs( mesh );

// 使用QuantizePosEncoding降低記憶體數
GeometryCompressionUtils.compressPositions( mesh );

// 使用NormEncodingMethods降低記憶體數
// [ "None", "DEFAULT", "OCT1Byte", "OCT2Byte", "ANGLES" ]
GeometryCompressionUtils.compressNormals( mesh, 'None' );

3. 建立由點到線的幾何體

var geometry = new THREE.BufferGeometry();
var material = new THREE.LineBasicMaterial( { vertexColors: true, morphTargets: true } );

var positions = [];
var colors = [];

for ( var i = 0; i < segments; i ++ ) {

 var x = Math.random() * r - r / 2;
 var y = Math.random() * r - r / 2;
 var z = Math.random() * r - r / 2;

 // positions

 positions.push( x, y, z );

 // colors

 colors.push( ( x / r ) + 0.5 );
 colors.push( ( y / r ) + 0.5 );
 colors.push( ( z / r ) + 0.5 );

}

geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
geometry.computeBoundingSphere();

line = new THREE.Line( geometry, material );
scene.add( line );

效果圖如下

線物資

是不是覺得這個程式碼與第一章節的程式碼十分類似呢, 實際上就是完全一樣的程式碼

不同點在於

  1. 線幾何體的 material 是THREE.LineBasicMaterial
  2. 建立線幾何體mesh使用的是 THREE.Line, 而點雲使用的是THREE.Points

有了建立點幾何體的知識, 就能建立線幾何體

4. 建立由線到面的幾何體

// 點數
var triangles = 160000;

var geometry = new THREE.BufferGeometry();

var positions = [];
// 點的法向量
var normals = [];
var colors = [];

var color = new THREE.Color();

// 正方體, 長寬高都為800
var n = 800, n2 = n / 2; 
// 三角形三個點點在正方體內, 這個正方體長寬高都為12
var d = 12, d2 = d / 2; 

// abc, 三個頂點位置
var pA = new THREE.Vector3();
var pB = new THREE.Vector3();
var pC = new THREE.Vector3();
// c點到b點的方向向量
var cb = new THREE.Vector3();
// a點到b點的方向向量
var ab = new THREE.Vector3();

for ( var i = 0; i < triangles; i ++ ) {

 // positions

 var x = Math.random() * n - n2;
 var y = Math.random() * n - n2;
 var z = Math.random() * n - n2;

 var ax = x + Math.random() * d - d2;
 var ay = y + Math.random() * d - d2;
 var az = z + Math.random() * d - d2;

 var bx = x + Math.random() * d - d2;
 var by = y + Math.random() * d - d2;
 var bz = z + Math.random() * d - d2;

 var cx = x + Math.random() * d - d2;
 var cy = y + Math.random() * d - d2;
 var cz = z + Math.random() * d - d2;

 // 新增一個三角形的3個頂點, 每個頂點有xyz三個資料
 positions.push( ax, ay, az );
 positions.push( bx, by, bz );
 positions.push( cx, cy, cz );

 // 求法向量, 首先設定三角形的三個頂點
 pA.set( ax, ay, az );
 pB.set( bx, by, bz );
 pC.set( cx, cy, cz );
 // 求出兩個方向向量
 cb.subVectors( pC, pB );
 ab.subVectors( pA, pB );
 // 叉積, 求法向量
 cb.cross( ab );
 // 單位化這個法向量
 cb.normalize();

 var nx = cb.x;
 var ny = cb.y;
 var nz = cb.z;
 // 新增法向量到法向量陣列中
 // 三角形的三個頂點的法向量相同, 因此複製三份
 normals.push( nx, ny, nz );
 normals.push( nx, ny, nz );
 normals.push( nx, ny, nz );

 // colors

 var vx = ( x / n ) + 0.5;
 var vy = ( y / n ) + 0.5;
 var vz = ( z / n ) + 0.5;

 color.setRGB( vx, vy, vz );

 colors.push( color.r, color.g, color.b );
 colors.push( color.r, color.g, color.b );
 colors.push( color.r, color.g, color.b );

}

// 加入位置資訊
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ).onUpload( disposeArray ) );
// 加入法向量資訊
geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ).onUpload( disposeArray ) );
// 加入顏色資訊
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ).onUpload( disposeArray ) );

geometry.computeBoundingSphere();

var material = new THREE.MeshPhongMaterial( {
 color: 0xaaaaaa, specular: 0xffffff, shininess: 250,
 side: THREE.DoubleSide, vertexColors: true
} );

mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

效果圖如下

幾何體面

面幾何體與前兩種幾何體很大的不同在於, 面幾何體需要法向量資訊

在程式碼中我也是新增了很多註釋便於理解, 這裡我再大致解釋一下

已知三個點, 求出兩條邊的方向向量, 這兩個方向向量做叉乘, 結果變為由三個點構成的三角形的法向量

5. 建立點雲的原始碼

由點到線, 由線到面, 希望讀者自己可以模仿寫出來

<!DOCTYPE html>
<html lang="ch">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    body{
      margin: 0;
      overflow: hidden;
    }
  </style>
</head>
<body>
<div id="container">

</div>

<script type="module">
  import * as THREE from '../build/three.module.js';
  import {OrbitControls} from "./jsm/controls/OrbitControls.js";
  import {GUI} from "./jsm/libs/dat.gui.module.js";

  let container, camera, scene, renderer, stats;

  let points;

  init();
  animation();

  function init() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x050505);
    scene.fog = new THREE.Fog(0x050505, 2000, 3000);
    scene.add(new THREE.AmbientLight(0x8FBCD4, 0.4));

    container = document.getElementById('container');
    camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 5, 3500);
    camera.position.z = 2750;
    scene.add(camera);

    let particles = 500000;

    let geometry = new THREE.BufferGeometry();

    let positions = [];
    let colors = [];

    let color = new THREE.Color();

    let n = 1000, n2 = n / 2;

    for (let i = 0; i < particles; i++) {

      let x = Math.random() * n - n2;
      let y = Math.random() * n - n2;
      let z = Math.random() * n - n2;

      positions.push(x, y, z);

      let vx = (x / n) + 0.5;
      let vy = (y / n) + 0.5;
      let vz = (z / n) + 0.5;
      color.setRGB(vx, vy, vz);
      colors.push(color.r, color.g, color.b);
    }

    geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));

    geometry.computeBoundingSphere();

    let material = new THREE.PointsMaterial({size:15, vertexColors: true});

    points = new THREE.Points(geometry, material);
    scene.add(points);

    // let pointLight = new THREE.PointLight(0xffffff, 1);
    // // 燈跟著相機走, 效果不錯
    // camera.add(pointLight);

    scene.add(new THREE.AxesHelper(5));

    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    let controls =  new OrbitControls(camera, renderer.domElement);
    controls.enabledZoom = false;

    window.addEventListener('resize', onWindowResize, false);
  }

  function animation(){
    render();

    requestAnimationFrame(animation);
  }

  function render() {

    let time = Date.now() * 0.001;

    points.rotation.x = time * 0.25;
    points.rotation.y = time * 0.5;

    renderer.render(scene, camera);
  }

  function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
  }


</script>
</body>
</html>

相關文章