three.js开发一般是比较套路的——init() + animate()
- init()时把所有的场景摆放好
- animate()就是一个递归调用的渲染过程。
如何实现整个场景的搭建?
- 初始化场景(Scene)、相机(Camera)、渲染器(Render)、控件(control)自然不必多说——都是three.js基本操作,有非常固定的模板
- 一个模型是怎么形成的?以场景之一的地球仪为例:step1先新建一个球形的几何体(Geometry);step2接着新建一个标准材质(Material),然后给材质贴图(图片diffuse+凹凸感bump+粗糙感roughness);step3最后一就可以通过几何体+材质,生成一个网格模型(Mesh)啦!!!
- 如果不使用封装好的模型呢?那么我们将看到这些模型最原始的模样——矩阵。原生的GL代码实现一个立方体,我们需要把它分解为6个面,每个面又是一个矩阵;其实这和直接调用封装类也没啥区别,比如给出三个点、再给出一个方向向量,一个立方体所有点在空间中的位置就确定了——只是这个计算过程不需要我们写出而已
- 关于两个光源——灯泡点状光就是空间中一个向外辐散射线(或者说向量)的点,主要参数是强度和衰减;户外半球光模拟的是就是太阳射向地球的光照,这无限接近于平行光直射,主要参数有光强,甚至还模拟类真实的户外,天空光和地面光的双重作用
- 关于镜子——镜子就是先新建一个镜子载体,然后在这个载体上涂上反射(Reflect)材料。反射类THREE.Reflector是已经封装好了的,如果想要用原生GL代码实现,思路也不难:通过镜子平面做法线,找到摄像机关于平面法线的对称位置,将一个虚拟摄像机(virtualCamera)放在那里,将它渲染出的图形作为纹理贴到镜面就行啦!——这个过程中,矩阵计算将发挥极大的作用
▶ 变量声明
var scene, camera, renderer |
场景、相机、渲染器 |
var controls |
控件 |
var metLoader |
材质加载器 |
var stats; |
状态显示类(FPS/显存占用) |
var gui; |
设置选择栏(光强1/光强2/曝光/阴影/显示镜子) |
var bulbLight, hemiLight |
光照(灯泡点状光+户外半球光) |
var floorGeometry, floorMat, floorMesh |
地板(几何体 + 材质 = 网格模型) |
var ballGeometry, ballMat, ballMesh |
地球仪(几何体 + 材质 = 网格模型) |
var boxGeometry, boxMat, boxMesh |
木盒子(几何体 + 材质 = 网格模型) |
var cyGeometry, cyMat, cyMesh |
木圆柱(几何体 + 材质 = 网格模型) |
var wallGeometry, wallMat, wallMesh |
墙(几何体 + 材质 = 网格模型) |
var mirrorGeometry, mirror |
镜子载体 + 镜子 |
▶ 关键函数
init(){...} |
初始化 |
function animate(){...} |
动画递归 |
unction render(){...} |
渲染 |
代码可直接运行。结构非常清晰。注释非常非常非常详细。
▽▽▽ index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>BULBtitle>
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="loli/three.js">script>
<script src="loli/stats.min.js">script>
<script src="loli/dat.gui.min.js">script>
<script src="loli/OrbitControls.js">script>
<script src="loli/Detector.js">script>
<script src="loli/Reflector.js">script>
<script src="samarua.js">script>
head>
<body>
<div id="container">div>
<script>
init();
animate();
script>
body>
html>
▽▽▽ style.css
body {
margin: 0;
}
▽▽▽ samarua.js
// >_< !
// 场景、相机、渲染器————three.js老三样
var scene, camera, renderer;
// 控件
var controls;
// 材质加载器
var metLoader;
// 时钟
var clock;
// 光照(灯泡点状光+户外半球光)
var bulbLight, hemiLight;
// 灯泡是整个场景的主角儿,我们给它加上几何体和材质
var bulbGeometry;
var bulbMat;
// 地板(几何体 + 材质 = 网格模型)
var floorGeometry, floorMat, floorMesh;
// 地球仪(几何体 + 材质 = 网格模型)
var ballGeometry, ballMat, ballMesh;
// 木盒子(几何体 + 材质 = 网格模型)
var boxGeometry, boxMat, boxMesh;
// 木头圆柱(几何体 + 材质 = 网格模型)
var cyGeometry, cyMat, cyMesh;
// 墙(几何体 + 材质 = 网格模型)
var wallGeometry, wallMat, wallMesh;
// 镜子载体+镜子
var mirrorGeometry, mirror;
// 状态显示类(FPS/显存占用)
var stats;
// 设置选择栏(光强1/光强2/曝光/阴影/显示镜子)
var gui;
//【灯泡点状光】
var bulbLightPowers = {
// 1瓦(W)
// 流明(lum/lm/lux/lx)
"110000 lux (1000W)": 110000,
"3500 lux (300W)": 3500,
"1700 lux (100W)": 1700,
"800 lux (60W)": 800,
"400 lux (40W)": 400,
"180 lux (25W)": 180,
"20 lux (4W)": 20,
"Off": 0
}
//【户外半球光】
var hemiLightPowers = {
"0.0001 lux (无月之夜)": 0.0001,
"0.5 lux (月圆之夜)": 0.5,
"3.4 lux (路灯)": 3.4,
"50 lux (阴雨)": 50,
"100 lux (客厅)": 100,
"350 lux (聚光灯)": 350,
"1000 lux (奥特曼)": 1000,
"50000 lux (核爆)": 50000
};
// 参数
var params = {
shadows: true,
exposure: 0.68,
bulbPower: Object.keys(bulbLightPowers)[4],
hemiPower: Object.keys(hemiLightPowers)[0],
mirror: false
};
/**
* 初始化
*/
function init() {
initSCRC(); // 初始化场景、相机、渲染器、控件
initPrepare(); // 初始化准备(始终、材质加载器)
initLight(); // 初始化光照(两种)
initFloor(); // 初始化地板
initBall(); // 初始化地球仪
initBox(); // 初始化木盒子
initCy(); // 初始化木圆柱
initWall(); // 初始化墙
initMirror(); // 初始化镜子
initMsg(); // 初始化状态信息
initGUI(); // 初始化选择栏
}
/**
* 初始化场景(Scene)、相机(Camera)、渲染器(Renderer)
* 初始化控件(Controls)
*/
function initSCRC() {
// 初始化场景
scene = new THREE.Scene();
// 初始化相机
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.x = -4;
camera.position.z = 4;
camera.position.y = 2;
// 初始化渲染器
renderer = new THREE.WebGLRenderer();
renderer.physicallyCorrectLights = true;
renderer.gammaInput = true;
renderer.gammaOutput = true;
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// 因为本Demo设计光影,因此渲染器更加复杂————(自上而下)允许光影精确渲染、伽玛色彩矫正、允许阴影、Reinhard经验公式、设置像素比
// 初始化控件
controls = new THREE.OrbitControls(camera, renderer.domElement);
}
/**
* 初始化准备
*/
function initPrepare() {
metLoader = new THREE.TextureLoader();
clock = new THREE.Clock();
}
/**
* 初始化光源(两个哦>_<)
*/
function initLight() {
bulbLight = new THREE.PointLight(0xffee88, 1, 100, 2); // 灯泡点状光(颜色、强度、距离、衰减)
hemiLight = new THREE.HemisphereLight(0xddeeff, 0x0f0e0d, 0.02); // 户外半球光(天空光颜色、地面光颜色、强度)
// 灯泡是整个场景的主角儿,我们给它加上几何体和材质
bulbGeometry = new THREE.SphereBufferGeometry(0.02, 16, 8);
bulbMat = new THREE.MeshStandardMaterial({
emissive: 0xffffee,
emissiveIntensity: 1,
color: 0x000000
});
bulbLight.add(new THREE.Mesh(bulbGeometry, bulbMat));
bulbLight.position.set(0, 2, 0);
bulbLight.castShadow = true;
// 别忘了放置到场景中!
scene.add(bulbLight);
scene.add(hemiLight);
}
// 下面,将要初始化场景中的网格模型们
// 一定记住这个三步走:网格模型(Mesh) = 几何体(Geometry) + 材质(Meterial)
/**
* 初始化地板
*/
function initFloor() {
// Step1————几何体
floorGeometry = new THREE.PlaneBufferGeometry(20, 20);
// Step2————材质
// Step2.1————标准材质(粗糙度/颜色/金属光泽/凹凸映射)
floorMat = new THREE.MeshStandardMaterial({
roughness: 0.8,
color: 0xffffff,
metalness: 0.2,
bumpScale: 0.0005
});
// Step2.2————贴图(图片diffuse+凹凸感bump+粗糙感roughness)
// http://www.yanhuangxueyuan.com/threejs/examples/textures/hardwood2_diffuse.jpg
metLoader.load("loli/img/hardwood2_diffuse.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping; // x轴方向重复映射
map.wrapT = THREE.RepeatWrapping; // y轴方向重复映射
map.anisotropy = 4;
map.repeat.set(10, 24); // 重复映射次数(x轴,y轴)————贴图不够大,所以要这样做
floorMat.map = map;
floorMat.needsUpdate = true;
});
metLoader.load("loli/img/hardwood2_bump.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(10, 24);
floorMat.bumpMap = map;
floorMat.needsUpdate = true;
});
metLoader.load("loli/img/hardwood2_roughness.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(10, 24);
floorMat.roughnessMap = map;
floorMat.needsUpdate = true;
});
// Step3————网格模型
floorMesh = new THREE.Mesh(floorGeometry, floorMat);
floorMesh.receiveShadow = true;
floorMesh.rotation.x = -Math.PI / 2.0;
// 还是别忘了加到场景中>_<!
scene.add(floorMesh);
// 之后初始化网格模型的注释就不再这么详细了,一定要记住三部曲————几何体 + 材质 = 网格模型
}
/**
* 初始化地球仪
*/
function initBall() {
ballGeometry = new THREE.SphereBufferGeometry(0.5, 32, 32);
ballMat = new THREE.MeshStandardMaterial({
color: 0x4169E1,
roughness: 0.5,
metalness: 1.0
});
metLoader.load("loli/img/earth_atmos_2048.jpg", function (map) {
map.anisotropy = 4;
ballMat.map = map;
ballMat.needsUpdate = true;
});
metLoader.load("loli/img/earth_specular_2048.jpg", function (map) {
map.anisotropy = 4;
ballMat.metalnessMap = map;
ballMat.needsUpdate = true;
});
ballMesh = new THREE.Mesh(ballGeometry, ballMat);
ballMesh.position.set(1, 0.5, 1);
ballMesh.rotation.y = Math.PI;
ballMesh.castShadow = true;
scene.add(ballMesh);
}
/**
* 初始化盒子
*/
function initBox() {
boxGeometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5);
boxMat = new THREE.MeshStandardMaterial({
roughness: 0.7,
color: 0xffffff,
bumpScale: 0.002,
metalness: 0.2
});
metLoader.load("loli/img/brick_diffuse.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(1, 1);
boxMat.map = map;
boxMat.needsUpdate = true;
});
metLoader.load("loli/img/brick_bump.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(1, 1);
boxMat.bumpMap = map;
boxMat.needsUpdate = true;
});
boxMesh = new THREE.Mesh(boxGeometry, boxMat);
boxMesh.position.set(-0.5, 0.25, -1);
boxMesh.castShadow = true;
scene.add(boxMesh);
}
/**
* 初始化木头圆柱
*/
function initCy() {
cyGeometry = new THREE.CylinderGeometry(0.3, 0.3, 1, 100, 100);
cyMat = new THREE.MeshStandardMaterial({
roughness: 0.8,
color: 0xffffff,
metalness: 0.2,
bumpScale: 0.0005
});
metLoader.load("loli/img/hardwood2_diffuse.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping; // x轴方向重复映射
map.wrapT = THREE.RepeatWrapping; // y轴方向重复映射
map.anisotropy = 4;
map.repeat.set(10, 24); // 重复映射次数(x轴,y轴)————贴图不够大,所以要这样做
cyMat.map = map;
cyMat.needsUpdate = true;
});
metLoader.load("loli/img/hardwood2_bump.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(10, 24);
cyMat.bumpMap = map;
cyMat.needsUpdate = true;
});
metLoader.load("loli/img/hardwood2_roughness.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(10, 24);
cyMat.roughnessMap = map;
cyMat.needsUpdate = true;
});
cyMesh = new THREE.Mesh(cyGeometry, cyMat);
cyMesh.position.x = -1.8;
cyMesh.position.y = 0;
cyMesh.position.z = 0;
cyMesh.castShadow = true;
scene.add(cyMesh);
}
/**
* 初始化墙
*/
function initWall() {
wallGeometry = new THREE.BoxBufferGeometry(10, 8, 0.5);
wallMat = new THREE.MeshStandardMaterial({
roughness: 0.7,
color: 0xffffff,
bumpScale: 0.002,
metalness: 0.2
});
metLoader.load("loli/img/brick_diffuse.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(1, 1);
wallMat.map = map;
wallMat.needsUpdate = true;
});
metLoader.load("loli/img/brick_bump.jpg", function (map) {
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
map.anisotropy = 4;
map.repeat.set(1, 1);
wallMat.bumpMap = map;
wallMat.needsUpdate = true;
});
wallMesh = new THREE.Mesh(wallGeometry, wallMat);
wallMesh.position.set(0, 0, -5);
wallMesh.castShadow = true;
scene.add(wallMesh);
}
/**
* 初始化镜子
*/
function initMirror() {
// 初始化
mirrorGeometry = new THREE.PlaneBufferGeometry(10, 8);
mirror = new THREE.Reflector(mirrorGeometry, {
clipBias: 0.003,
textrueWidth: window.innerWidth * window.devicePixelRatio,
textrueHeight: window.innerHeight * window.devicePixelRatio,
color: 0x777777,
recursion: 1
});
// 摆放好位置
// 镜像渲染不清晰...给放远一点...
mirror.position.x = 20;
mirror.position.y = 0;
mirror.position.z = 2;
mirror.rotation.y = -Math.PI / 2.0;
scene.add(mirror);
}
/**
* 初始化状态显示类(FPS/显存占用)
*/
function initMsg() {
stats = new Stats();
document.getElementById('container').appendChild(stats.dom);
}
/**
* 初始化设置栏
*/
function initGUI() {
gui = new dat.GUI();
gui.add(params, 'bulbPower', Object.keys(bulbLightPowers));
gui.add(params, 'hemiPower', Object.keys(hemiLightPowers));
gui.add(params, 'exposure', 0, 1);
gui.add(params, 'shadows');
gui.add(params, 'mirror');
gui.open();
}
/**
* 动画(本质是递归渲染)
*/
function animate() {
requestAnimationFrame(animate);
render();
}
/**
* 渲染
*/
var previousShadowMap = false;
function render() {
// 灯光扩散相关参数(渲染器的属性)
renderer.toneMappingExposure = Math.pow(params.exposure, 5.0);
// 灯光阴影相关参数
renderer.shadowMap.enabled = params.shadows;
bulbLight.castShadow = params.shadows;
if (params.shadows !== previousShadowMap) {
floorMat.needsUpdate = true;
ballMat.needsUpdate = true;
boxMat.needsUpdate = true;
previousShadowMap = params.shadows;
}
// 灯光亮度相关参数(灯光本身的属性)
bulbLight.power = bulbLightPowers[params.bulbPower];
bulbMat.emissiveIntensity = bulbLight.intensity / Math.pow(0.02, 2.0);
// 环境光的相关参数
hemiLight.intensity = hemiLightPowers[params.hemiPower];
// 镜子的位置(位置调远,假装镜子没了...)
mirror.position.x = params.mirror ? 5 : 100;
// 控制灯泡运动的函数(1.25的基础上,用一个cos进行调整)
var time = Date.now() * 0.0005;
bulbLight.position.y = Math.cos(time) * 0.75 + 1.25;
// 渲染
renderer.render(scene, camera);
// 左上角状态显示栏的更新
stats.update();
}
E N D END END