Three.JS入门

330 阅读8分钟

threeJs入门

结构

three.js 主要由 场景(scene)、相机(camera)和渲染器(render)组成; 相机就是我们看到的画面,场景是画面中的内容,渲染器是用来渲染画面的。 image.png

相机

相机是对场景进行取景,将我们需要看到的内容显示在屏幕上。相机分几大类,根据不同场景选择即可。 相机分类: image.png 相机区别: image.png

常用的相机是正投影相机(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();

光源

在我们创建的场景中,必须要有光源才能看得见物品场景中的内容,否则看到的是黑色物体 光源分类 image.png

创建光源 其中半球光和环境光都不会产生阴影,区别在于半球光更接近自然光,模仿从天空到地面的颜色渐变,而环境光只有一种颜色

/**
 * 光源设置
 * 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);

创建模型

模型是由 几何体和材质组成的网格模型,然后将网格模型加入到场景中,就会展现出我们期望的画面

  1. 网格模型

    //  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); //网格模型添加到场景中
    
  2. 材质

image.png

  1. 贴图 单一的材质对象并不能满足复杂的业务需求。所以大部分时候都需要使用贴图来显示复杂图像 贴图分类 image.png
  • 颜色贴图: 最简单的贴图,通过贴图加载器的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)

image.png

  • 法线贴图 可以使原本光滑的平面贴图产生凹凸感,如:地球上的山脉突起

使用方法:需要在原来贴图的基础上在添加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)

image.png

  • 凹凸贴图 也是为了使贴图产生凹凸感,但是没有法线贴图表达的几何体表面信息更丰富。和法线贴图的区别在于计算方法不同,凹凸贴图使用灰度计算高低深度 image.png
/**
 * 砖块
 * 凹凸贴图
 */
 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);

image.png 注意事项: 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)

image.png

  • 光照贴图(阴影贴图) 用来模拟光照下的阴影效果 需要设置模型、点光源以及周围环境的阴影贴图属性(是否接受阴影贴图) 和实时计算得到的阴影相比较通过贴图的方式更为节约资源,提高渲染性能 image.png
  1. 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) image.png

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);
}

image.png

更换材质示例

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() // 取消马达

约束 约束是对物理模拟下物体之间的一种约束

  1. 点对点 类似于小车上的轮子和车身之间的约束
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 );

image.png

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();

滑块约束...