什麼是Three.js?
- 百度百科上是這麼說的:
Three.js是JavaScript編寫的WebGL第三方庫。提供了非常多的3D顯示功能。運作在浏覽器中的 3D 引擎,你可以用它建立各種三維場景,包括了攝影機、光影、材質等各種對象。你可以在它的首頁上看到許多精彩的示範。不過,這款引擎還處在比較不成熟的開發階段,其不夠豐富的 API 以及匮乏的文檔增加了初學者的學習難度(尤其是文檔的匮乏)three.js的代碼托管在github上面。
一些有用的連結
- Three.js的基本概念:https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene
- 入門教程:https://threejsfundamentals.org/
- Github:https://github.com/mrdoob/three.js/tree/master
基本概念
- Tips:這裡隻作為核心概念的基本介紹,更詳細請閱讀上面連結的内容
- 要建立一個threejs應用,就必須了解組成threejs應用的基本概念:場景、相機、渲染器
- 相機:我們在螢幕上看場景内容的視圖工具,相當于我們的眼睛
- 場景:一些模型或者其它等所在的環境,相當于我們用眼睛看到的周圍的各種物體等的環境,我們建立的各種模型都是直接通過add函數加進這裡的
- 渲染器:負責把相機和場景渲染到浏覽器視圖上
環境準備(使用webpack搭建開發)
- 初始化nodejs項目
npm init -y
- 安裝webpack、webpack-cli
npm i --save-dev webpack
npm i --save-dev webpack-cli
- 安裝一些loader、plugin
npm i --sece-dev @babel/core
npm i --sece-dev @babel/plugin-transform-runtime
npm i --sece-dev @babel/preset-env
npm i --sece-dev babel-loader
npm i --sece-dev css-loader
npm i --sece-dev html-loader
npm i --save-dev clean-webpack-plugin
npm i --save-dev html-webpack-plugin
npm i --save-dev copy-webpack-plugin
- 建立并配置 webpack.config.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebPackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
devtool: 'eval-cheap-module-source-map',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 9000,
host: '0.0.0.0',
hot: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{ loader: 'babel-loader', options: { presets: [['@babel/preset-env', { useBuiltIns: 'usage' }]] } }
]
},
{
test: /\.css$/i,
exclude: /node_modules/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.html$/,
loader: 'html-loader'
},
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebPackPlugin({ template: './src/index.html' }),
new webpack.HotModuleReplacementPlugin(),
new CopyWebpackPlugin(
{ patterns: [{ from: path.resolve(__dirname, 'public'), to: 'public' }] }
)
]
}
- 之後添加一些目錄和檔案,整個項目的結構如下圖;其中
目錄是一些需要通路的資源(如圖檔、3D模型等);/public
是一些其靜态資源;/src/assets
是業務代碼;/src/common
是模闆html;/src/index.html
是入口js/src/index.html
Three.js入門之做一個簡單的3D場景内添加标點的功能 - 最後安裝最關鍵的
Threejs
npm i --save three
開始開發
- Tips:代碼裡面用到的
的函數是筆者自行封裝的,在文章底部有本文源碼連結Utils.XXX
- 在
裡面添加一個html
的容器元素Three.js
<style>
- {
margin: 0;
padding: 0;
}
html {
overflow: hidden !important;
height: 100vh;
width: 100vw;
}
body, #canvas {
height: 100%;
width: 100%;
}
</style>
...一些其他代碼
<canvas id="canvas"></canvas>
...一些其他代碼
- 建立一個
并在test.js
導入使用index.js
- 在
中導入必要的依賴test.js
// 導入threejs子產品
import * as THREE from 'three';
// 由threejs官方提供的驗證浏覽器是否支援webgl的工具函數,需要到https://github.com/mrdoob/three.js/blob/master/examples/jsm/WebGL.js擷取
import { WEBGL } from './WebGL.js';
// 軌道控制器,用來給場景添加可用滑鼠來移動旋轉場景的功能
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// 變換控制器,用來給某一個模型添加可以用滑鼠來在場景内移動該模型的功能
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
- 建立場景、相機、渲染器、燈光(Tips:這裡的代碼以及之後的代碼都是在 WEBGL.isWebGLAvailable() 驗證了浏覽器有WebGL功能後編寫的)
// 使用Utils工具裡面的init函數初始化場景、相機、渲染器
let { scene, camera, renderer } = Utils.init(
{ bgColor: 0xf0f0f0 },
{
fov: 85,
// 記得在html裡加一個canvas元素,這個元素是渲染出來的視圖的容器
aspect: document.getElementById('canvas').innerWidth / document.getElementById('canvas').innerHeight,
near: 0.1,
far: 100000
}
);
// Utils.init 實作細節
function init (
sceneConfig = {
// 場景的背景色
bgColor: 0xeeeeee
},
cameraConfig = {
// 相機的視野角度
fov: 75,
// 相機的寬高比
aspect: document.getElementById('canvas').innerWidth / document.getElementById('canvas').innerHeight,
// 近截面(物體某些部分比錄影機的遠截面遠或者比近截面近的時候,該這些部分将不會被渲染到場景中)
near: 0.1,
// 遠截面
far: 1000
},
rendererConfig = {
// 渲染器挂載的dom容器
canvas: document.getElementById('canvas')
}
) {
// 建立場景
const scene = new THREE.Scene();
// 設定場景的背景色
scene.background = new THREE.Color(sceneConfig.bgColor);
// 建立相機
const camera = new THREE.PerspectiveCamera( cameraConfig.fov, cameraConfig.aspect, cameraConfig.near, cameraConfig.far );
// 建立渲染器
const renderer = new THREE.WebGLRenderer(rendererConfig);
return { scene, camera, renderer };
}
// 建立完基本的 scene, camera, renderer 後,要設定相機的位置,不然相機會在(0, 0, 0)的位置;然後添加燈光,沒有燈光的話,可能會看不到我們添加的模型
// 相機位置
camera.position.set( 0, 250, 1000 );
// 給場景添加一個環境光
scene.add( new THREE.AmbientLight( 0xf0f0f0 ) );
- 添加軌道控制器
// 建立一個軌道控制器執行個體,傳入剛剛建立的的相機執行個體以及渲染器的容器dom對象
const controls = new OrbitControls(camera, renderer.domElement);
// 設定旋轉的中心點
controls.target.set(0, 0, 0);
- 添加變換控制器
// 建立一個變換控制器執行個體,傳入剛剛建立的的相機執行個體以及渲染器的容器dom對象
let transformControl = new TransformControls( camera, renderer.domElement );
// transformControl的dragging(拖動事件)發生時改變就控制一下軌道控制器啟用禁用(因為要拖拽目前的模型,是以要禁用旋轉)
transformControl.addEventListener( 'dragging-changed', ( event ) => {
controls.enabled = !event.value;
} );
// 添加變換控制器到場景裡
scene.add( transformControl )
- 添加一個底座平面,并且在這個底座上添加一些網格,以便标點參考位置
// 添加一個底座平面
// 平面幾何
const planeGeometry = new THREE.PlaneGeometry( 2000, 2000 );
// 把xy平面變為xZ平面
planeGeometry.rotateX( - Math.PI / 2 );
// 基礎網格材質
const planeMaterial = new THREE.MeshBasicMaterial();
// 把平面幾何和基礎網格材質 生成平面網格
const plane = new THREE.Mesh( planeGeometry, planeMaterial );
// 平面網格向下(y軸負方向)移動200機關
plane.position.y = -200;
// 把平面添加到場景裡面
scene.add( plane );
// 網格輔助器
// 建立一個網格輔助器的執行個體,傳入參數 坐标格尺寸、坐标格細分次數
const helper = new THREE.GridHelper( 2000, 100 );
// 向下(y軸負方向)移動199機關,與底座平面幾乎重合
helper.position.y = - 199;
// 透明度
helper.material.opacity = 0.25;
// 是否可透明
helper.material.transparent = true;
// 添加到場景
scene.add( helper );
- 添加一個立方體在(0, 0, 0),給标點做參考物體
// 建立以一個立方體
const geometry = new THREE.BoxGeometry(50, 50, 50);
// 建立一個網格材質
const material = new THREE.MeshPhongMaterial( { color: 0x00ff00 } );
// 把立方體和材質添加到一個網格中
const cube = new THREE.Mesh( geometry, material );
// 設定立方體的位置
cube.position.set(0, 0, 0);
// 把網格添加到場景
scene.add( cube );
- 定義添加标點的工廠函數,并初始化一個預設标點
// 一個存儲标點執行個體對象模型的數組(給标點添加事件時有用)
let objArr = []
// 利用紋理加載器,加載一個圖檔,用來做标點的樣式
const map = new THREE.TextureLoader().load( "/public/icon.png" );
// 利用這個圖檔建立一個精靈圖材質(無論在哪個視角看,精靈圖材質的模型都是面向我們的),sizeAttenuation屬性是讓模型不随視圖内容的縮小放大而縮小放大
const spriteMaterial = new THREE.SpriteMaterial( { map: map, sizeAttenuation: false } );
// 建立第二個精靈圖材質,depthTest是讓這個模型被其它模型遮擋仍然能被看見(預設被遮住時不能透過模型被看見),opacity設定透明度(為什麼要弄兩個材質?為了讓标點被遮住時有被遮住的效果)
const spriteMaterial2 = new THREE.SpriteMaterial( { map: map, sizeAttenuation: false, depthTest: false, opacity: 0.2 } );
// 建立精靈圖模型執行個體的函數
function createMarker (m) {
return new THREE.Sprite( m );
}
// 建立一個标點的函數
function createMarkerCon() {
// 第一個精靈圖模型
let sprite1 = createMarker(spriteMaterial)
// 第二個精靈圖模型
let sprite2 = createMarker(spriteMaterial2)
// 第一個精靈圖模型 把 第二個精靈圖模型 添加為子模型
sprite1.add(sprite2)
// 設定精靈圖模型的尺寸縮放
sprite1.scale.set(0.1, 0.1, 0.1);
// 設定精靈圖模型初始位置
sprite1.position.set(100, 100, 0);
// 因為場景裡不可能隻有标點,是以要對精靈圖模型添加特異性字段進行區分
sprite1.isMarker = true;
// 把第一個精靈圖模型添加到場景
scene.add(sprite1);
// 把标點(第一個精靈圖模型)添加到objArr
objArr.push(sprite1);
}
// 建立一個标點
createMarkerCon()
- 定義擷取所有标點位置的函數
let getPosition = () => {
// 周遊 objArr 數組
for (let i = 0; i < objArr.length; i++) {
// 建立一個三維空間的點對象
let p = new THREE.Vector3();
// 把标點相對于世界(場景)的坐标複制到 p
objArr[i].getWorldPosition(p);
console.log('--------------- -- ');
console.log('marker - ', i);
console.log(p);
}
alert('位置資訊已在控制台輸出');
}
- 在html裡面添加兩個按鈕,添加标點的按鈕 和 擷取标點位置的按鈕,給兩個按鈕添加點選事件
.btn {
position: absolute;
top: 25px;
right: 25px;
background-color: #000;
padding: 5px;
color: #fff;
cursor: pointer;
border-radius: 5px;
}
.btn-2 { top: 60px; }
.btn-3 { top: 95px; }
...一些其他代碼
<div class="btn" id="add">Add Popup Marker</div>
<div class="btn btn-2" id="get">Get Position</div>
...一些其他代碼
let add = document.getElementById('add');
let get = document.getElementById('get');
add.onclick = createMarkerCon;
get.onclick = getPosition;
- 給标點添加滑鼠的點選、拖拽等事件,以便能利用滑鼠對标點位置進行調整。在threejs的視圖裡面不進行一些轉換,是沒辦法監聽模型的事件的,要利用Raycaster來計算焦點,擷取哪個模型與射線相交,進而讓模型觸發事件
// 建立一個射線執行個體對象
let raycaster = new THREE.Raycaster();
// 建立一個二維空間點的對象(x,y),在進行将滑鼠位置歸一化為裝置坐标時(x 和 y 方向的取值範圍是 (-1 to +1))有用
let mouse = new THREE.Vector2();
// 存儲 滑鼠按下時的二維空間點
let onDownPosition = new THREE.Vector2();
// 存儲 滑鼠松開時的二維空間點
let onUpPosition = new THREE.Vector2();
// 滑鼠在移動時觸發的事件
let onPointermove = ( event ) => {
// 通過 Utils.onTransitionMouseXYZ 函數把将滑鼠位置歸一化為裝置坐标(實作細節請直接看Utils工具類)
mouse = Utils.onTransitionMouseXYZ(event, renderer.domElement);
// 通過錄影機和滑鼠位置更新射線
raycaster.setFromCamera( mouse, camera );
// 計算模型和射線的焦點(objArr就是之前存儲标點模型的數組)
var intersects = raycaster.intersectObjects(objArr);
// 擷取到有焦點的模型的數組後,對于不是目前 transformControl 變換器正在變換的模型的焦點模型,把這個模型添加到 transformControl ,讓目前變換的模型為擷取到焦點的模型
if ( intersects.length > 0 ) {
const object = intersects[ 0 ].object;
if ( object !== transformControl.object ) {
transformControl.attach( object );
}
}
}
// 滑鼠按鍵按下時觸發的事件
let onPointerdown = ( event ) => {
onDownPosition.x = event.clientX;
onDownPosition.y = event.clientY;
}
// 滑鼠按鍵松開時觸發的事件(相當于點選事件觸發)
let onPointerup = ( event ) => {
onUpPosition.x = event.clientX;
onUpPosition.y = event.clientY;
// 如果滑鼠按鍵按下和松開的時候是在同一個點同一個位置,則取消 transformControl 變換器正在變換的模型的變化狀态,然後觸發點選事件
if ( onDownPosition.distanceTo( onUpPosition ) === 0 ) {
transformControl.detach();
onClick(event)
}
}
// 點選事件(在onPointerup函數裡調用)
let onClick = (event) => {
// 通過 Utils.onTransitionMouseXYZ 函數把将滑鼠位置歸一化為裝置坐标(實作細節請直接看Utils工具類)
let mouse = Utils.onTransitionMouseXYZ(event, renderer.domElement);
// 通過錄影機和滑鼠位置更新射線
raycaster.setFromCamera( mouse, camera );
// 計算模型和射線的焦點(objArr就是之前存儲标點模型的數組)
let intersects = raycaster.intersectObjects(objArr);
// 如果有相交的标點模型,就做一些事情,比如顯示彈窗(這不是threejs的内容,不進行介紹,要在html裡面加一個彈窗元素,直接看代碼即可)
if ( intersects.length > 0 ) {
const object = intersects[ 0 ].object;
if (object.isMarker) {
// 彈窗内容
let info = document.getElementById('info');
info.style = 'display: inline-block;top: ' + (event.clientY - 50) + 'px;left: ' + (event.clientX + 50) + 'px;'
// info.innerHTML='<iframe src="http://localhost:9000/" frame style="width: 100%;height: 300px"></iframe>'
// 計算合适的彈窗大小和位置
let body = document.getElementById('html');
setTimeout(() => {
body.scrollTop = 1;
body.scrollLeft = 1;
if (body.scrollTop) {
info.style.top = event.clientY - info.clientHeight + 50 + 'px';
if (event.clientY < info.clientHeight) {
let { num2 } = numLow10(event.clientX, info.clientWidth)
info.style.height = num2 + 100 + 'px';
info.style.top = event.clientX - num2 + 'px';
}
}
if (body.scrollLeft) {
info.style.left = event.clientX - info.clientWidth - 50 + 'px';
if (event.clientX < info.clientWidth) {
let { num2 } = numLow10(event.clientX, info.clientWidth)
info.style.width = num2 - 100 + 'px';
info.style.left = event.clientX - num2 + 'px';
}
}
}, 10)
// 如果 num1 < num2 num2就減少10 的遞歸函數
function numLow10 (num1, num2) {
console.log(num1, num2)
if (num1 < num2)
return numLow10(num1, num2 - 10)
else
return { num1, num2 }
}
}
} else {
info.style = 'display: none';
}
}
// 添加事件委托
window.addEventListener( 'pointermove', onPointermove, false );
window.addEventListener( 'pointerdown', onPointerdown, false );
window.addEventListener( 'pointerup', onPointerup, false );
// Utils.onTransitionMouseXYZ 實作細節
// 将滑鼠位置歸一化為裝置坐标。x 和 y 方向的取值範圍是 (-1 to +1)
function onTransitionMouseXYZ( event, domElement ) {
let mouse = new THREE.Vector2();
let domElementLeft = domElement.getBoundingClientRect().left
let domElementTop = domElement.getBoundingClientRect().top
mouse.x = ((event.clientX - domElementLeft) / domElement.clientWidth) * 2 - 1
mouse.y = -((event.clientY - domElementTop) / domElement.clientHeight) * 2 + 1
return mouse;
}
- 最後,雖然上面添加了很多東西,但是還是缺少很重要的一步,現在頁面是看不到效果的,因為還沒有利用初始化好的
對象來把相機和場景渲染到視圖上。現在來完成這一步renderer
function animate() {
// 在浏覽器重繪之前渲染(requestAnimationFrame是什麼?請看:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame)
requestAnimationFrame( animate );
// 在視圖的大小發生變化的時候,是需要更新相機的寬高比的,不然看到的場景會發生變形,使用Utils.updateCameraAspect實作(實作細節請直接看Utils工具類)
Utils.updateCameraAspect(renderer, camera);
// 用渲染器把 場景 和 相機 渲染到頁面
renderer.render( scene, camera );
}
animate();
// Utils.updateCameraAspect 實作細節
// 看看寬高是否有變化,就有更新寬高比
function updateCameraAspect (renderer, camera) {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
// 畫布的寬高動态設定
function resizeRendererToDisplaySize (renderer) {
const canvas = renderer.domElement;
const pixelRatio = window.devicePixelRatio;
const width = canvas.clientWidth * pixelRatio | 0;
const height = canvas.clientHeight * pixelRatio | 0;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
- 最終效果
Three.js入門之做一個簡單的3D場景内添加标點的功能
源碼連結
- https://github.com/LChi1103/threejs-dome