3D世界-Three.js的探究與實踐

此文的github及當中示例:piexl.github.io/explore-thr…css

項目需求

在開發育空網站時咱們須要在頁面內使用四面體,還要增長一些交互和操做,所以使用了3D類庫Three.js也經歷多種波折才實現了咱們最終的效果。html

須要的實現的交互: 1.四面體自動旋轉; 2.旋轉過程當中監聽當前用戶正對那個面 3.四面體點擊某個面時跳轉到這個面對應的連接 4.實現粒子背景git

3D世界的初認識

Three.js官方文檔使用起來對於初學者來講並不優化,經過下面連個Three.js的先關資料對它的理解能更塊些github

Three.js 文檔結構
Three.js 文檔結構: 圖片來自

Three.js 核心對象結構和基本的渲染流程
Three.js 核心對象結構和基本的渲染流程: 圖片來自

右手座標系 Three.js 採用的是的右手座標系,座標系的原點在畫布中心(canvas.width / 2, canvas.height / 2)。咱們能夠經過 Three.js 提供的 THREE.AxisHelper() 輔助方法將座標系可視化。RGB顏色分別表明 XYZ 軸,以下圖canvas

右手座標系

右手座標系:圖片來自api

建立一個場景

準備html,引入Three.js數組

<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8>
    <title>My first three.js app</title>
    <style> body { margin: 0; } canvas { width: 100%; height: 100% } </style>
</head>
<body>
    <script src="https://cdn.bootcss.com/three.js/r83/three.js"></script>
</body>
</html>
複製代碼

建立場景架構

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
複製代碼

上面的代碼構建了scene, camera 和 renderer。Three.js的架構支持多種camera,這裏使用最多見的遠景相機(PerspectiveCamera),也就是相似於人眼觀察的方式。第一個屬性75設置的是視角(field of view)。 第二個屬性設置的是相機拍攝面的長寬比(aspect ratio)。咱們幾乎老是會使用元素的寬除以高,不然會出現擠壓變形。 接下來的2個屬性是近裁剪面(near clipping plane) 和 遠裁剪面(far clipping plane)。下面這張圖能夠幫助你理解:app

攝像機

建立立方體dom

var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( { color: 0x2185D0 } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );
複製代碼

渲染場景

在大多數屏幕上,刷新率通常是60次/秒

function animate() {
    requestAnimationFrame( animate );
    renderer.render( scene, camera );
}
animate();
複製代碼

使立方體動起來

將下列代碼添加到animate()函數中renderer.render調用的上方:

cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
複製代碼

最終實現效果

3D世界的深層瞭解

四面體的實現嘗試一(引入模型法)

效果見頁面引入模型法

優勢:實現方式簡單,貼圖完整 缺點: 1.沒法設置模型的中心,致使旋轉中心誤差模型異常 2.材質貼圖後期修改麻煩,沒法分開修改 3.點擊事件沒法增長

準備素材

  • 四面體模型
  • 材質貼圖
  • obj模型導入庫OBJLoader.js

實現方法

構建創景及場景參數設置

//場景設置
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
camera.position.z = 20;
var renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true //canvas是否包含alpha
});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
複製代碼

其中WebGLRendereralpha參數爲場景的背景透明度,爲true則透明能可看到場後後面的背景。

場中輔助線、控制器及環境光的設置

//座標輔助線
var axesHelper = new THREE.AxesHelper( 100 );
scene.add( axesHelper );

//控制器
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;

//建立一個環境光,並增長到場景中
var ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
複製代碼

加載模型

//加載模型
var mesh;
var manager = new THREE.LoadingManager(); //加載管理器
//導入模型紋理Texture
var texture = new THREE.Texture();
var loader = new THREE.ImageLoader(manager);
loader.load('../imgs/obj/texture.jpg', function (image) {
    texture.image = image;
    texture.needsUpdate = true;
});
// 建立loader變量,用於導入模型
var loader = new THREE.OBJLoader(manager);
//第一個表示模型路徑,第二個表示完成導入後的回調函數,通常咱們須要在這個回調函數中將導入的模型添加到場景中
loader.load('../imgs/obj/tetrahedron.obj', function (obj) {
    obj.traverse(function (child) {
        if (child instanceof THREE.Mesh) {
            child.material.side = THREE.DoubleSide;
            child.material.map = texture;
        }
    });
    mesh = obj;//儲存到全局變量中
    mesh.position.y = 0;
    scene.add(mesh);//將導入的模型添加到場景中
    animate();
});
複製代碼

ImageLoader爲圖片加載器,OBJLoader爲obj對象模型加載器,並在模型加載回調中執行渲染函數

渲染模型

function animate(){
    requestAnimationFrame(animate);
    if (mesh.rotation.y < Math.PI * 2) {
        mesh.rotation.y += 0.01;
        mesh.rotation.x += 0.01;
    }
    renderer.render(scene, camera);
}
複製代碼

渲染時更改模型的角度,讓模型轉起來

[注] 加載模型的方法不少,具體根據你製做模型的軟件和導出的模型的格式而定,obj形式只是其中一中而已

四面體的實現嘗試二(幾何四面體實現)

效果見頁面幾何四面體實現

優勢:實現簡單,大小,材質等參數更改方便 缺點:貼圖無規則,沒法貼圖

實現步驟

構件場景並設置基本參數

//構建場景
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
camera.position.z = 30;
var renderer = new THREE.WebGLRenderer({
    alpha: true
});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
複製代碼

座標線、控制器及環境光的增長

// 座標線
var axesHelper = new THREE.AxesHelper( 100 );
scene.add( axesHelper );

//控制器
var orbit = new THREE.OrbitControls( camera, renderer.domElement );
orbit.enableZoom = false;

//設置環境光
var ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
複製代碼

建立四面體模型,並設置材質

// 建立模型
var mesh = new THREE.Object3D()
//設置線段樣式
mesh.add( new THREE.LineSegments(
    new THREE.Geometry(),
    new THREE.LineBasicMaterial( {
        color: 0xffffff,
        transparent: true,
        opacity: 0.5
    } )
) );

//設置模型的材質
mesh.add( new THREE.Mesh(
    new THREE.Geometry(),
    new THREE.MeshPhongMaterial( {
        map: new THREE.TextureLoader().load('../imgs/texture.jpg?'+new Date().getTime()),
    } )
));
//四面幾何體的類,第一個參數爲四面體的半徑,第二個參數增長的單數,若是不爲這不是四面體
var geometry = new THREE.TetrahedronGeometry(14, 0);
mesh.children[ 0 ].geometry.dispose();
mesh.children[ 1 ].geometry.dispose();
mesh.children[ 0 ].geometry = new THREE.WireframeGeometry( geometry );
mesh.children[ 1 ].geometry = geometry;
scene.add( mesh );
複製代碼

增長渲染器和輔助線

//包圍盒的輔助對象
var box = new THREE.BoxHelper( mesh, 0xffff00 );
scene.add( box );

//渲染器
var render = function () {
    requestAnimationFrame( render );
    var time = Date.now() * 0.001;
    if(mesh.rotation.y <= Math.PI*2){
        mesh.rotation.x += 0.005;
        mesh.rotation.y += 0.005;
    }
    renderer.render( scene, camera );
};
render();
複製代碼

增長模型的輔助線更有利於咱們對模型的理解

四面體的實現嘗試三(頂點構造法)

效果見頁面頂點構造法

優勢:材質貼圖可分開,中心點穩定不會偏移 缺點:構造複雜,運算效率低

實現步驟

構建場景

//構建場景
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.z = 30;
var renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
複製代碼

增長控制器、環境光及座標輔助線,事件處理插件的初始化

//控制器
var orbit = new THREE.OrbitControls( camera, renderer.domElement );
orbit.enableZoom = false;

//環境光
var ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);

//座標輔助線
var axesHelper = new THREE.AxesHelper( 100 );
scene.add( axesHelper );

//事件處理初始化
var interaction = new THREE.Interaction(renderer, scene, camera);
複製代碼

建立四面體的每一個頂點及構造四個面

利用下面的這張圖有助於咱們理解這個四面體的構建,設定一個基本單位r,四面的四個點頂點位置以下圖:

構建四面體的點

//定義幾何點
var cubeGeometry = new THREE.Geometry();
//建立四面的頂點
var r = 8;
var vertices = [
    new THREE.Vector3(r, r, r), //v0
    new THREE.Vector3(-r, -r, r), //v1
    new THREE.Vector3(-r, r, -r), //v2
    new THREE.Vector3(r, -r, -r), //v3
];
//設置四面體的各個座標點
cubeGeometry.vertices = vertices;
//建立立方的面,各個面的排列順序
var faces=[
    new THREE.Face3(0,1,2),
    new THREE.Face3(1,3,2),
    new THREE.Face3(2,3,0),
    new THREE.Face3(3,1,0),
];
cubeGeometry.faces = faces;
複製代碼

爲四面體每一個面設置貼圖

//給四面體設置貼圖
var materials = [];  //建立一個貼圖數組
//設置貼圖數組
for(var i = 0;i < cubeGeometry.faces.length;i++){
    materials[i] = new THREE.MeshBasicMaterial({
        // map: new THREE.TextureLoader().load('../imgs/texture' + (i+1) + '.jpg?'+new Date().getTime()),
        map: new THREE.TextureLoader().load('../imgs/texture_new-0' + (i+1) + '.jpg?'+new Date().getTime()),
        side: THREE.DoubleSide,
    })
}
//記錄每一個面的id,將紋理座標和faceid間接關聯,不然紋理圖片始終都是第一張的圖片
var faceId = 0;
var uv = [
    new THREE.Vector2(0,0), //圖片左下角
    new THREE.Vector2(1,0), //圖片右下角
    new THREE.Vector2(1,1), //圖片右上角
    new THREE.Vector2(0,1), //圖片左上角
];
//設置紋理座標
for(var m=0;m<cubeGeometry.faces.length;m+=1){
    cubeGeometry.faces[m].materialIndex = faceId;
    cubeGeometry.faceVertexUvs[0][m] = [uv[0],uv[1],uv[2]];
    faceId++;
}
複製代碼

建立四面體併爲其增長輔助線

//建立四面體
var cube = new THREE.Mesh(cubeGeometry,materials);

//增長盒輔助線
var box = new THREE.BoxHelper( cube, 0xffff00 );
scene.add( box );
複製代碼

爲四面體增長各面的法線點

要尋找過四面體每一個面過中心點的法線,即過此面的中心且垂直於這個面的點構成的線. 以四面體ABC面爲例:

四面體面法線
C爲ABC面的中心, ∵ Bb ⊥ cb 且 eb ⊥ Bb ∴ Bb ⊥ bce ∴ Bb ⊥ ec 同理可證 Ba ⊥ ec ∵ Bb ⊥ ec 且 Ba ⊥ ec ∴ ec ⊥ ABC 因此 ec爲面ABC的法線,e爲ABC面法線上的點

其餘面的法線點以下圖中(efgh):

各面的法線點

//四個面的法線點
var pointsGeometry = new THREE.Geometry();
var normalPoints = [
    new THREE.Vector3(-r, r, r),
    new THREE.Vector3(-r, -r, -r),
    new THREE.Vector3(r, r, -r),
    new THREE.Vector3(r, -r, r)
];
pointsGeometry.vertices = normalPoints;
var pointsMaterial = new THREE.PointsMaterial( {
    color: 0xfffffff,
    size:0.5,
} );
var pointsField = new THREE.Points( pointsGeometry, pointsMaterial );
複製代碼

把四面體和法線點放到同一個組中

var group = new THREE.Group();
group.add( cube );
group.add( pointsField );
group.autoRate = true;
scene.add( group );
複製代碼

監聽當前正對到那個面

四面體轉動過程當中法線點的新座標獲取:

當四面體繞繞Y軸轉動轉動(Y座標不發生變化): Y軸準東的角度:ralationY = group.rotation.y 開始時在水平面的角度:startY = Math.abs(Math.atan(x/z)) 那麼Y軸總角度爲:degY = startY + ralationY 水平面轉動的半徑爲:fR = Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2)) 轉動後打的點x座標爲: x = fR * (z / x > 0 ? Math.sin(degY) : Math.cos(degY)) * (x > 0 ? 1 : -1)

以法線點e(-r, r, r)和f(-r, -r, -r)爲例參考下圖:

法線點Y軸轉動變化參考

當四面體繞繞X軸轉動轉動(X座標不發生變化): X軸準東的角度:ralationX = group.rotation.x 開始時在豎直面的角度:startX = Math.abs(Math.atan(y/z)), 那麼X軸總角度爲:degX = startX + ralationX 豎直面轉動的半徑爲:sR = Math.sqrt(Math.pow(y, 2) + Math.pow(z, 2)) 轉動後打的點y座標爲: y = sR * (z / y > 0 ? Math.cos(degX) : Math.sin(degX)) * (y > 0 ? 1 : -1)

以法線點e(-r, r, r)和f(-r, -r, -r)爲例參考下圖:

法線點X軸轉動變化參考

//法線點的初始位置
var originalPonits = [
    new THREE.Vector3(-r, r, r),
    new THREE.Vector3(-r, -r, -r),
    new THREE.Vector3(r, r, -r),
    new THREE.Vector3(r, -r, r)
];
//監聽當前屬於哪一個面
function checkCurFace(){
    var degCell = 2 * Math.PI / 360;
    var distance = [];//與相機位置的距離
    originalPonits.forEach(function (item, index) {
        var itemOldPoint = normalPoints[index];
        var x = itemOldPoint.x,
            y = itemOldPoint.y,
            z = itemOldPoint.z,
            startX = Math.abs(Math.atan(y/z)),
            startY = Math.abs(Math.atan(x/z)),
            ralationX = group.rotation.x,
            ralationY = group.rotation.y,
            degX = startX + ralationX,
            degY = startY + ralationY,
            fR = Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2)),//底部投影半徑
            sR = Math.sqrt(Math.pow(y, 2) + Math.pow(z, 2)),//側面投影半徑
            cR = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)); //向量點距中心的半徑

        item.x = fR * (z / x > 0 ? Math.sin(degY) : Math.cos(degY)) * (x > 0 ? 1 : -1);
        item.z = sR * (z / y > 0 ? Math.sin(degX) : Math.cos(degX)) * (z > 0 ? 1 : -1);
        item.y = sR * (z / y > 0 ? Math.cos(degX) : Math.sin(degX)) * (y > 0 ? 1 : -1);

        var distanceX = camera.position.x - item.x,
            distanceY = camera.position.y - item.y,
            distanceZ = camera.position.z - item.z;
        distance.push(Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2) + Math.pow(distanceZ, 2)));
    });
    var minDistance = Math.min.apply(null, distance),
        curIndex = distance.indexOf(minDistance);
    console.log('curIndex', '當前面的序號爲:'+curIndex);
}
複製代碼

這個檢查函數中使用了立體結合的一些知識,最終根據轉動時每一個法線點座標更新,得到它與相機兩點點的距離,距離最短的就是當前的這是激活面。

給四面體增長事件

//給四面體增長事件
cube.on('click', function (ev) {
    //點擊檢查當前的激活面
    checkCurFace();
});
cube.on('mouseover', function (ev) {
    //鼠標進入四面體組中止轉動
    group.autoRate = false;
});
cube.on('mouseout', function (ev) {
    //鼠標離開四面體組開始轉動
    group.autoRate = true;
});
複製代碼

增長渲染器讓四面體旋轉起來

//渲染器
var render = function () {
    requestAnimationFrame( render );
    var time = Date.now() * 0.001;
    if(group.autoRate){
        if(group.rotation.x < Math.PI*2){
            group.rotation.x += 0.01;
            group.rotation.y += 0.01;
        }else{
            group.rotation.x = 0;
            group.rotation.y = 0;
        }
    }
    checkCurFace();
    renderer.render( scene, camera );
};
render();
複製代碼

在每次更新是檢查當前屬於那個面

粒子背景的實現

效果見頁面粒子背景

實現方法

構建場景

//建立場景
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 2000);
camera.position.z = 1000;
var renderer = new THREE.WebGLRenderer({
    alpha: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild( renderer.domElement );
複製代碼

增長光源

//光源添加到場景中
var ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
複製代碼

建立星空點

//建立星空點
var starsGeometry = new THREE.Geometry();
for ( var i = 0; i < 1000; i ++ ) {
    var star = new THREE.Vector3();
    star.x = Math.random() * 2000 - 1000;
    star.y = Math.random() * 2000 - 1000;
    star.z = Math.random() * 2000 - 1000;
    starsGeometry.vertices.push( star );
}
var textureLoader = new THREE.TextureLoader();
var sprite = textureLoader.load('../imgs/snowflake1.png');
var PointSizes = [2,3,4,5,6,7,8,9];
var starsMaterial = new THREE.PointsMaterial( {
    color: 0xfffffff,
    size: PointSizes[parseInt(Math.random()*7)],
    map: sprite,
} );
var starField = new THREE.Points( starsGeometry, starsMaterial );
scene.add( starField );
複製代碼

粒子類Points( geometry, material )

渲染場景

// 渲染器
function render() {
    var time = Date.now() * 0.00005;
    camera.position.x += camera.position.x * 0.05;
    camera.position.y += camera.position.y * 0.05;
    camera.lookAt(scene.position);
    for (var i = 0; i < scene.children.length; i++) {
        var object = scene.children[i];
        if (object instanceof THREE.Points) {
            object.rotation.y = time * (i < 4 ? i + 1 : -(i + 1));
        }
    }
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}
render();
複製代碼

每次渲染的時候修改相機的x和y的位置,讓星空轉起來。修改每一個點的y座標讓點有自身的變化。

參考連接

相關文章
相關標籤/搜索