threeJs入门
结构
three.js 主要由 场景(scene)、相机(camera)和渲染器(render)组成;
相机就是我们看到的画面,场景是画面中的内容,渲染器是用来渲染画面的。
相机
相机是对场景进行取景,将我们需要看到的内容显示在屏幕上。相机分几大类,根据不同场景选择即可。
相机分类:
相机区别:
常用的相机是正投影相机(OrthographicCamera),模拟人物视角使用透视相机
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
对于相机最常用的是通过鼠标操作对某个模型进行观察,官方提供了一个插件(OrbitControlsJS),添加完该插件后,就可以通过鼠标以及键盘来实现对摄像机的操作(平移,旋转)
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//执行渲染操作 指定场景、相机作为参数
// animate()
/**
* 使用OrbitControls 插件
* 可以使用方向键以及鼠标控制摄像机 移动、平移、缩放、拖动
*
*/
function render() {
renderer.render(scene, camera);
// 和监听事件冲突,二者选一即可
requestAnimationFrame(render);
}
render()
// 鼠标控制事件
var controls = new THREE.OrbitControls(camera, renderer.domElement);//创建控件对象
// controls.addEventListener('change', render);//监听鼠标、键盘事件 和requestAnimationFrame事件冲突,二者选一即可
渲染器
通过对应的渲染器渲染出我们制作好的场景, 最常用的渲染器为WebGLRender,通过webGL渲染
let renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//执行渲染操作 指定场景、相机作为参数
animate()
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
场景
我们的主要设计部分都是在场景中进行的,所包含的内容也非常多 创建场景
var scene = new THREE.Scene();
光源
在我们创建的场景中,必须要有光源才能看得见物品场景中的内容,否则看到的是黑色物体
光源分类
创建光源 其中半球光和环境光都不会产生阴影,区别在于半球光更接近自然光,模仿从天空到地面的颜色渐变,而环境光只有一种颜色
/**
* 光源设置
* AmbientLight 环境光
* PointLight 点光源
* DirectionalLight 平行光,比如太阳光
* SpotLight 聚光源
*/
//点光源 从一个点向各个方向发射的光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
// 环境光 均匀的照亮场景中的所有物体。
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
创建模型
模型是由 几何体和材质组成的网格模型,然后将网格模型加入到场景中,就会展现出我们期望的画面
-
网格模型
// var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象 //创建一个立方体几何对象 //长方体 参数:长,宽,高 // var geometry = new THREE.BoxGeometry(100, 100, 100); // 球体 参数:半径60 经纬度细分数40,40 // var geometry = new THREE.SphereGeometry(60, 40, 40); // 圆柱 参数:圆柱面顶部、底部直径50,50 高度100 圆周分段数 // var geometry = new THREE.CylinderGeometry( 50, 50, 100, 25 ); // 正八面体 // var geometry = new THREE.OctahedronGeometry(50); // 正十二面体 // var geometry = new THREE.DodecahedronGeometry(50); /** * 材质对象 * * MeshBasicMaterial 基础网格材质,不受光照影响的材质 * MeshLambertMaterial Lambert网格材质,与光照有反应,漫反射 * MeshPhongMaterial 高光材质,与光照有反应 * MeshStandardMaterial PBR物理材质,相比较高光材质可以更好的模拟金属、玻璃等效果 */ let material = new THREE.MeshLambertMaterial({ // let material = new THREE.MeshLambertMaterial({ color: 0x444444, }); //材质对象Material let mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh (把材质添加到对应几何对象中之后组合成的对象) scene.add(mesh); //网格模型添加到场景中 -
材质
- 贴图
单一的材质对象并不能满足复杂的业务需求。所以大部分时候都需要使用贴图来显示复杂图像
贴图分类
- 颜色贴图: 最简单的贴图,通过贴图加载器的load方法加载图片之后可以直接使用。网格模型会使用颜色贴图上的rgb值进行渲染,所以不需要设置color属性
// 颜色贴图(map
let geometryGrass = new THREE.PlaneGeometry(100, 100, 100)
const textureGrass = new THREE.TextureLoader().load( './image/grass.png' )
// 设置阵列模式
textureGrass.wrapS = THREE.RepeatWrapping;
textureGrass.wrapT = THREE.RepeatWrapping;
// 两个方向纹理重复数量
textureGrass.repeat.set( 20, 20 );
let materialGrass = new THREE.MeshBasicMaterial({
map: textureGrass,
}); //材质对象Material
let plane = new THREE.Mesh(geometryGrass, materialGrass)
plane.position.set(0, 110, 0)
scene.add(plane)
- 法线贴图 可以使原本光滑的平面贴图产生凹凸感,如:地球上的山脉突起
使用方法:需要在原来贴图的基础上在添加normalMap配置
/**
* 地球
* 法线贴图
*/
let geometryEarth = new THREE.SphereBufferGeometry(100, 40, 40)
let textureEarth = new THREE.TextureLoader().load( './image/earth.jpg' )
let textureEarthNormal = new THREE.TextureLoader().load( './image/earth_normal.jpg' )
let materialEarth = new THREE.MeshPhongMaterial({
map: textureEarth,
normalMap: textureEarthNormal,
color: 0xffffff,
}); //材质对象Material
let earth = new THREE.Mesh(geometryEarth, materialEarth)
earth.position.set(200, 0, 100)
scene.add(earth)
- 凹凸贴图
也是为了使贴图产生凹凸感,但是没有法线贴图表达的几何体表面信息更丰富。和法线贴图的区别在于计算方法不同,凹凸贴图使用灰度计算高低深度
/**
* 砖块
* 凹凸贴图
*/
let geometry = new THREE.BoxGeometry(100, 100, 100);
let geometry1 = new THREE.BoxGeometry(100, 100, 100);
const texture = new THREE.TextureLoader().load( './image/brick.jpg' )
let textureBrick = new THREE.TextureLoader().load( './image/brick_bump.jpg' )
let material = new THREE.MeshPhongMaterial({
map: texture,
bumpMap: textureBrick,
bumpScale: 2 // 凹凸贴图高度 默认为1
});
let material1 = new THREE.MeshPhongMaterial({
map: texture,
});
let mesh = new THREE.Mesh(geometry, material);
let mesh1 = new THREE.Mesh(geometry1, material1);
mesh1.position.set(120, 0, 0)
scene.add(mesh);
scene.add(mesh1);
注意事项:
1、MeshLambertMaterial、MeshBasicMaterial 没有凹凸、法线贴图属性
高光网格材质MeshPhongMaterial、标准网格材质MeshStandardMaterial和物理网格材质MeshPhysicalMaterial支持法线贴图normalMap功能
2、只设置环境光的情况下,没有办法查看到法线贴图和凹凸贴图的效果。
- 环境贴图 通过贴图来展示周围的环境,提高渲染性能,比如天空盒
// 环境贴图
let geometry = new THREE.CubeGeometry(900, 900, 900)
const nameArr = ['posx', 'negx', 'posy', 'negy', 'posz', 'negz']
const materialArr = []
nameArr.forEach(item => {
materialArr.push(new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load(`./image/skyBox/${item}.jpg`),
side: THREE.BackSide
}))
})
let mesh = new THREE.Mesh(geometry, materialArr)
scene.add(mesh)
- 光照贴图(阴影贴图)
用来模拟光照下的阴影效果
需要设置模型、点光源以及周围环境的阴影贴图属性(是否接受阴影贴图)
和实时计算得到的阴影相比较通过贴图的方式更为节约资源,提高渲染性能
- 3D复杂模型 对于一些复杂模型都是通过建模软件制作完成之后再导入。 官网推荐使用gltf格式进行导入。优点是使用流式传输,加载快,渲染性能高; 可以使用模型自带的场景、相机动画等。
对于3d模型需要引入对应格式的加载器,并调用loader方法 导入成功之后会将导入的3d模型作为对象参数传入,将其添加进scene即可
const loader = new GLTFLoader();
loader.load( 'path/to/model.glb', function ( gltf ) {
scene.add( gltf.scene );
}, undefined, function ( error ) {
console.error( error );
} );
3d对象中有一个traverse方法,该方法会遍历自身以及下级,传入一个参数,为当前遍历的对象
group.traverse((obj) => {
if (obj.visible === false) {
obj.visible = true
}
})
模型动画播放
在导入的3d模型中有一个animation属性,保存着该模型的动画数组
动画需要使用动画混合器进行播放(animationMixer)
const loader = new GLTFLoader(); // gltf格式的加载器
const mixer = new THREE.AnimationMixer(model); // 动画混合器
loader.load( 'path/to/model.glb', function ( gltf ) {
mixer = new THREE.AnimationMixer(gltf);
const action = mixer.clipAction(clip);
action.play() // 播放动画
scene.add( gltf.scene );
});
// 动画间切换的过渡
function fadeToAction(name, duration) {
previousAction = activeAction;
activeAction = actions[name];
if (previousAction !== activeAction) {
previousAction.fadeOut(duration);
}
activeAction
.reset()
.setEffectiveTimeScale(1)
.setEffectiveWeight(1)
.fadeIn(duration)
.play();
}
event
对于一些交互行操作需要通过事件来触发对应的行为。 但是threejs渲染在canvas中的时候通过点击事件得到的只是一个canvas对象。 所以需要通过射线类(raycaster)来进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2(); // 原点坐标
// 获取鼠标坐标并转化为原点坐标
function onPointerMove(event) {
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
}
window.addEventListener('pointermove', onPointerMove);
// 射线需要实时更新
function render() {
// 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(pointer, camera);
renderer.render(scene, camera);
// 和监听事件冲突,二者选一即可
requestAnimationFrame(render);
}
更换材质示例
let selectObj = null // 当前选中对象的引用
window.addEventListener('click', (e) => {
e.preventDefault();
if (selectObj) {
selectObj = null
}
// 获取射线上的物体 true为遍历子对象
const arrs = raycaster.intersectObject(group, true)
if (arrs.length) {
const resObject = arrs.filter(res => res && res.object)[0]
if (resObject && resObject.object) {
selectObj = resObject.object
console.log(`当前选中模型name属性为 "${selectObj.name}"`)
console.log(selectObj, 'selectobj');
// 对不同模型进行特殊化操作
if (selectObj.name === 'mesh') {
setBumpMap(selectObj)
} else if (selectObj.name === 'mesh1') {
// 初始化
selectObj.material = material2
} else if (selectObj.geometry.type === 'PlaneGeometry') {
// 修改材质
mesh1.material = selectObj.material
}
// 更新材质
materialRefresh()
}
}
function setBumpMap(object) {
object.material.bumpMap = textureBrick
object.material.bumpScale = 2
// 通知渲染器需要更新视图
object.material.needsUpdate = true
}
function materialRefresh() {
mesh1.material.needsUpdate = true
}
})
控制物体移动示例 通过键盘事件对物体进行坐标设置
window.addEventListener('keyup', e => {
if (selectObj && e.ctrlKey) {
objectMove(e.code)
}
function objectMove(code) {
const moveFnModel = {
ArrowRight,
ArrowLeft,
ArrowDown,
ArrowUp,
Space
}
moveFnModel[code] && moveFnModel[code]()
selectObj.position.needsUpdate = true
}
function ArrowRight() {
selectObj.position.set(selectObj.position.x + 50, selectObj.position.y, selectObj.position.z)
}
function ArrowLeft() {
selectObj.position.set(selectObj.position.x - 50, selectObj.position.y, selectObj.position.z)
}
function ArrowDown() {
selectObj.position.set(selectObj.position.x, selectObj.position.y, selectObj.position.z - 50)
}
function ArrowUp() {
selectObj.position.set(selectObj.position.x, selectObj.position.y, selectObj.position.z + 50)
}
function Space() {
let speed = 0
let isUp = true
let isJumping = true
const HEIGHT = 2
jump()
// 模拟跳跃
function jump() {
if (isUp) {
speed += 0.015
isUp = speed <= 1.3
} else {
speed -= 0.015
isJumping = speed >= 0
}
if (isJumping) {
if (isUp) {
selectObj.position.set(selectObj.position.x, selectObj.position.y + HEIGHT * speed, selectObj.position.z)
} else {
selectObj.position.set(selectObj.position.x, selectObj.position.y - HEIGHT * speed, selectObj.position.z)
}
requestAnimationFrame(jump)
}
}
}
})
物理引擎
threeJs自身并没有物理引擎这个概念,所以我们需要引入一个插件(physiJs)来实现物理引擎
CannonJs、OimoJS、EnergyJs
// 配置physiJS
Physijs.scripts.worker = 'js/physijs_worker.js';
Physijs.scripts.ammo = './ammo.js';
// 需要使用物理场景替代threeJS的原生场景
let scene = new Physijs.Scene()
scene.setGravity(new THREE.Vector3(0, -30, 0)); // 设置重力方向以及大小
// physiJs中的事件 进行物理模拟时触发
scene.addEventListener(
'update',
function() {
// 通知场景更新
scene.simulate( undefined, 1 )
}
);
// 需要使用给定的物理材质以及物理网格对象
let geometry = new THREE.BoxGeometry( 50, 50, 50 )
let material1 = new Physijs.createMaterial(
new THREE.MeshBasicMaterial({map: new THREE.TextureLoader().load('./image/brick.jpg'),})
, 1 // 摩擦力系数
, 1 // 回弹系数
)
let mesh = new Physijs.BoxMesh(geometry, material1);
scene.simulate()
scene.add(mesh)
在物理场景中如果使用了group,group中的内容不会具有物理特性,所以需要进行物体组合时,直接将子物体添加到父物体上即可(使用父物体的add方法)
马达
/*
axis:绕某个轴转动,0是x轴,1是y轴,2是z轴
low,high:旋转角度的范围,将low设置稍微比high稍高,则可以自由转动
speed:转动的速度
force:施加的力大小
*/
configAngularMotor(axis,low,high,speed,force) // 设置力的属性
enableAngularMotor(speed,acceleration) //启动马达 速度 加速度
disableMotor() // 取消马达
约束 约束是对物理模拟下物体之间的一种约束
- 点对点 类似于小车上的轮子和车身之间的约束
var constraint = new Physijs.PointConstraint(
physijs_mesh_a, // First object to be constrained
physijs_mesh_b, // OPTIONAL second object - if omitted then physijs_mesh_1 will be constrained to the scene
new THREE.Vector3( 0, 10, 0 ) // point in the scene to apply the constraint
);
scene.addConstraint( constraint );
2.铰链约束 类似于开门关门的效果
var constraint = new Physijs.HingeConstraint(
physijs_mesh_a, // First object to be constrained
physijs_mesh_b, // OPTIONAL second object - if omitted then physijs_mesh_1 will be constrained to the scene
new THREE.Vector3( 0, 10, 0 ), // point in the scene to apply the constraint
new THREE.Vector3( 1, 0, 0 ) // Axis along which the hinge lies - in this case it is the X axis
);
scene.addConstraint( constraint );
constraint.setLimits(
low, // 最小角度
high, // 最大角度
bias_factor, // applied as a factor to constraint error
relaxation_factor, // 来回抖动系数
);
constraint.enableAngularMotor( target_velocity, acceration_force );
constraint.disableMotor();
滑块约束...