此文的github及當中示例:https://piexl.github.io/explore-threejs/css
在開發育空網站時咱們須要在頁面內使用四面體,還要增長一些交互和操做,所以使用了3D類庫Three.js也經歷多種波折才實現了咱們最終的效果。html
須要的實現的交互:
1.四面體自動旋轉;
2.旋轉過程當中監聽當前用戶正對那個面
3.四面體點擊某個面時跳轉到這個面對應的連接
4.實現粒子背景
Three.js官方文檔使用起來對於初學者來講並不優化,經過下面連個Three.js的先關資料對它的理解能更塊些git
Three.js 文檔結構:圖片來自github
Three.js 核心對象結構和基本的渲染流程:圖片來自canvas
右手座標系
Three.js 採用的是的右手座標系,座標系的原點在畫布中心(canvas.width / 2, canvas.height / 2)。咱們能夠經過 Three.js 提供的 THREE.AxisHelper() 輔助方法將座標系可視化。RGB顏色分別表明 XYZ 軸,以下圖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>
建立場景app
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)。下面這張圖能夠幫助你理解: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;
效果見頁面引入模型法
優勢:實現方式簡單,貼圖完整
缺點:
1.沒法設置模型的中心,致使旋轉中心誤差模型異常
2.材質貼圖後期修改麻煩,沒法分開修改
3.點擊事件沒法增長
準備素材
//場景設置 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 );
其中WebGLRenderer
的alpha
參數爲場景的背景透明度,爲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)爲例參考下圖:
當四面體繞繞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)爲例參考下圖:
//法線點的初始位置 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座標讓點有自身的變化。