threejs 入门基础(万字总结)(建议收藏!!!)

官方例子:

three.js examples

官方 api 文档:

three.js docs

通过github下载最新的threejs

GitHub - mrdoob/three.js: JavaScript 3D Library.

打开后按照从code按钮处打开就可以下载最近的压缩包

如何在代码中引入threejs

.html文件中直接引入threejs

如果不是正式开发Web3D项目,只是学习threejs功能,完全没必要用webpack或vite搭建一个开发环境。

学习使用的环境,只要创建一个.html文件,编写threejs代码,最后通过本地静态服务打开.html文件就行。

type="importmap"配置路径

学习环境中,.html文件引入three.js,最好的方式就是参考threejs官方案例,通过配置<script type="importmap">,实现学习环境.html文件和vue或reaact脚手架开发环境一样的写法。这样你实际项目的开发环境,不用改变threejs引入代码。

官方入门案例

通过官方提供的demo方便我们线下进行几个基础概念的理解,主要这些基础概念理解了我们后续的学习就会非常顺利了。案例当中设计的概念有 场景Scene相机Camera渲染器Renderer,着色器/几何体 Geometry,材料 Mertial 5个基本概念。

三维场景Scene

你可以把三维场景Scene(opens new window)对象理解为虚拟的3D场景,用来表示模拟生活中的真实三维场景,或者说三维世界。

// 创建3D场景对象Scene
const scene = new THREE.Scene();

着色器/几何体Geometry

字面意思是几何体,但是这里的创建的几何体并不会直接的在页面当中渲染,只是几个毛坯的几何体,还需要配合材料光照等因素才能具体成型。

//创建一个长方体几何对象Geometry
const geometry = new THREE.BoxGeometry(1, 1, 1); 

材质Material 控制物体的外观

如果你想定义物体的外观效果,比如颜色,就需要通过材质Material相关的API实现。

threejs不同材质渲染效果不同,下面就以threejs最简单的网格基础材质MeshBasicMaterial(opens new window)为例给大家实现一个红色材质效果。

//创建一个材质对象Material
const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,//0xff0000设置材质颜色为红色
}); 

使用几何体和材质创建具体的网格模型

使用Mesh方法把集合和材料组合在一起形成最终要渲染的模型实体

const cube= new THREE.Mesh(geometry, material); //网格模型对象Mesh

.add()方法 添加模型到场景中

在threejs中你创建了一个表示物体的虚拟对象Mesh,需要通过.add()方法,把网格模型mesh添加到三维场景scene中。

scene.add(mesh);

相机

透视投影相机PerspectiveCamera

Threejs提供了正投影相机OrthographicCamera(opens new window)和透视投影相机PerspectiveCamera(opens new window)。本节课先给大家比较常用的透视投影相机PerspectiveCamera。

透视投影相机PerspectiveCamera本质上就是在模拟人眼观察这个世界的规律。

// 实例化一个透视投影相机对象
const camera = new THREE.PerspectiveCamera();

相机位置.position

生活中用相机拍照,你相机位置不同,拍照结果也不同,threejs中虚拟相机同样如此。

比如有一间房子,你拿着相机站在房间里面,看到的是房间内部,站在房子外面看到的是房子外面效果。

相机对象Camera具有位置属性.position,通过位置属性.position可以设置相机的位置。

camera.position.z = 5

渲染器

生活中如果有了景物相机,那么如果想获得一张照片,就需要你拿着相机,按一下,咔,完成拍照。对于threejs而言,如果完成“咔”这个拍照动作,就需要一个新的对象,也就是WebGL渲染器WebGLRenderer(opens new window)

WebGL渲染器WebGLRenderer

通过WebGL渲染器WebGLRenderer(opens new window)可以实例化一个WebGL渲染器对象。

// 创建渲染器对象
const renderer = new THREE.WebGLRenderer();

设置Canvas画布尺寸.setSize() 

// 定义threejs输出画布的尺寸(单位:像素px)
const width = 800; //宽度
const height = 500; //高度
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)

渲染器渲染方法.render()

渲染器WebGLRenderer执行渲染方法.render()就可以生成一个Canvas画布(照片),并把三维场景Scene呈现在canvas画布上面,你可以把.render()理解为相机的拍照动作“咔”。

renderer.render(scene, camera); //执行渲染操作

渲染器Canvas画布属性.domElement

渲染器WebGLRenderer通过属性.domElement可以获得渲染方法.render()生成的Canvas画布,.domElement本质上就是一个HTML元素:Canvas画布。

document.body.appendChild(renderer.domElement);

执行渲染

通过 renderer.render( scene ,camera) 我们可以在界面当中直接看到我们创建的场景和3d模型。 不过为了更好的直观的看到3d模型的整体状态,我们加上了一些动画效果。

   function animate(){
            // requestAnimationFrame 是js内置的方法,表示在动画结束之后重新调用一个指定的函数
            requestAnimationFrame(animate);
            cube.rotation.x +=0.01; // x轴旋转
            cube.rotation.y +=0.01; // y轴旋转
            // 渲染器执行render方法来执行最终的渲染
            renderer.render( scene ,camera)
        }
  animate(); 
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        // 场景 用来展示3d内容的场景 
        const scene = new THREE.Scene();
        // 相机 用来代替我们的眼睛去观察物体,通过相机摆放在不同的位置来实现对于3d物体的不同角度的效果的呈现
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        // 渲染器 我们的场景和相机都是一个虚拟的概念,如果要把内容呈现到网页当中,需要通过渲染器来进行渲染。
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的大小
        document.body.appendChild(renderer.domElement); // 把渲染器的内容加载成为一个body的子元素 
        // 着色器 一个基础的几何体,并不是最终要展示的内容而是一个等待被描述的几何体(毛坯)
        // const geometry = new THREE.BoxGeometry(2, 1, 1);
        // const geometry = new THREE.CircleGeometry(1, 30);
        const geometry = new THREE.CylinderGeometry(1, 1, 2, 30);
        // 材质 用于对着色器表面材料的说明、
        const material = new THREE.MeshBasicMaterial({
            color: 0xf67f12 // // 0xff0000 等同于 css当中的 #ff0000
        });
        // 物体  Mesh 网格模型 。网格模型就是最终需要展示的3d物体
        const cube = new THREE.Mesh(geometry, material);
        // 在场景scene当中添加 创建好的物体
        scene.add(cube);
        // 设置相机的位置
        camera.position.z = 5;
        function animate() {
            // requestAnimationFrame 是js内置的方法,表示在动画结束之后重新调用一个指定的函数
            requestAnimationFrame(animate);
            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;
            // 渲染器执行render方法来执行最终的渲染
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

相机控件 ObrbitControls

我们每次修改代码再去看效果其实对于3d的控件放置来说是不方便的,所以three提供了一个相机控件,允许我们通过鼠标的操作就能调整我们的相机的位置,所以快速观察3d模型的不同角度的效果

OrbitControls本质

OrbitControls本质上就是改变相机的参数,比如相机的位置属性,改变相机位置也可以改变相机拍照场景中模型的角度,实现模型的360度旋转预览效果,改变透视投影相机距离模型的距离,就可以改变相机能看到的视野范围

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }

    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 引入相机控件
        // 场景 用来展示3d内容的场景 
        const scene = new THREE.Scene();
        // 相机 用来代替我们的眼睛去观察物体,通过相机摆放在不同的位置来实现对于3d物体的不同角度的效果的呈现
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        // 渲染器 我们的场景和相机都是一个虚拟的概念,如果要把内容呈现到网页当中,需要通过渲染器来进行渲染。
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的大小
        document.body.appendChild(renderer.domElement); // 把渲染器的内容加载成为一个body的子元素 
        // 着色器 一个基础的几何体,并不是最终要展示的内容而是一个等待被描述的几何体(毛坯)
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        // 材质 用于对着色器表面材料的说明、
        const material = new THREE.MeshBasicMaterial({
            color: 0xff0000 // // 0xff0000 等同于 css当中的 #ff0000
        });
        // 物体  Mesh 网格模型 。网格模型就是最终需要展示的3d物体
        const cube = new THREE.Mesh(geometry, material);
        // 在场景scene当中添加 创建好的物体
        scene.add(cube);
        // 设置相机的位置
        camera.position.z = 5;
        const controls = new OrbitControls(camera, renderer.domElement);
        // 本质上 相机控制控制的就是相机的postion的位置而在界面当中出现不同的几何体的观察形态
        controls.addEventListener('change', () => {
            console.log('camera.position', camera.position);
            renderer.render(scene, camera)
        })
        renderer.render(scene, camera)
    </script>
</body>

</html>

三维坐标系的理解

在很多的设计软件中是左手坐标系(C4D)X轴指向右侧、Y轴指向上方、Z指向屏幕内。

在ThreeJS中是右手坐标系,X轴指向右侧、Y轴指向上方、Z指向屏幕外。

材质半透明设置

为了更好的观察threejs的坐标原点的情况,我们可以把集合体的材质设置为半透明。

const material = new THREE.MeshBasicMaterial( { 
  color: 0xff0000,
  transparent: true, //开启透明度
  opacity: 0.5,  // 设置具体的透明度为50%
});

渲染器设置为支持透明

        const renderer = new THREE.WebGLRenderer({
            alpha: true, // 背景是否可以设置透明色
            antialias: true, // 表示是否开启反锯齿
        });

使用辅助器来理解坐标空间

对于新手来说 需要适应3d的坐标空间,threejs提供了有一些辅助器来帮助我们对于空间位置关系进行更好的理解

AxesHelper 坐标辅助线

THREE.AxesHelper()的参数表示坐标系坐标轴线段尺寸大小,你可以根据需要改变尺寸。

// AxesHelper:辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);

GridHelper 网格辅助线

// 网格辅助线  第一个值是长度   第二个值是分割的段数
const gridHelper = new THREE.GridHelper(10,10);
scene.add(gridHelper)

设置模型在坐标系中的位置或尺寸

通过模型的位置、尺寸设置,加深3D坐标系的理解

// BoxGeometry(x,y,z) 传入的就是三个方向的大小的值
const geometry = new THREE.BoxGeometry( 2, 4, 1 ); 

改变相机参数

也可以通过调整相机的位置 加深3D坐标系的理解

camera.position.z = 5;
camera.position.x = 2
// camera.position.y = 5;
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({
            alpha: true, // 背景是否可以设置透明色
            antialias: true, //表示是否开启反锯齿
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        // BoxGeometry(x,y,z) 传入的就是三个方向的大小的值
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({
            color: 0xff0000,
            transparent: true, //开启透明度
            opacity: 0.5,  // 设置具体的透明度为50%
        });
        const cube = new THREE.Mesh(geometry, material);
        cube.position.x = 0.5
        cube.position.y = 0.5
        cube.position.z = 0.5
        scene.add(cube);
        // 右手坐标轴 大拇指指向自己
        camera.position.z = 5;
        camera.position.x = 2
        camera.position.y = 5;
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);// 在整个场景当中添加坐标辅助线
        // 网格辅助线  第一个值是长度   第二个值是分割的段数
        const gridHelper = new THREE.GridHelper(27, 27);
        scene.add(gridHelper)
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', () => {
            console.log('camera.position', camera.position);
            renderer.render(scene, camera)
        })
        renderer.render(scene, camera)
    </script>
</body>

</html>

光源的基础理解

实际生活中物体表面的明暗效果是会受到光照的影响,threejs中同样也要模拟光照Light对网格模型Mesh表面的影响。

材质与光源

基础网格材质MeshBasicMaterial(opens new window)不会受到光照影响。

漫反射网格材质MeshLambertMaterial(opens new window)会受到光照影响,该材质也可以称为Lambert网格材质,音译为兰伯特网格材质。

const geometry = new THREE.BoxGeometry(1, 1, 1);
// lambert的材料是受光照影响的
const material = new THREE.MeshLambertMaterial({
    color: 0xff0000
})
const cube = new THREE.Mesh(geometry, material)
// 没有添加光源之前 受光照影响的材料都会只是黑色的存在
scene.add(cube)
// 创建一个点光源 ( 点光源就是模拟一个灯泡发出的光源)
// PointLight( 光的颜色,光的强度 )
const pointLight = new THREE.PointLight(0xffffff, 1.0)
// pointLight.position.set(x轴,y轴,z轴) 设置光源的位置
// pointLight.position.set( 10,0,0);// v设置光源在x轴上
pointLight.position.set(100, 60, 50);
// 在场景当中添加光源
scene.add(pointLight)  

切换 scene.add( pointLight) 的注释 可以看到有光源和没光源的情况下漫反射材料的效果。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({
            // alpha: true, // 背景是否可以设置透明色
            antialias: true, //表示是否开启反锯齿
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        // lambert的材料是受光照影响的
        const material = new THREE.MeshLambertMaterial({
            // color: 0xff0000
            color: 0xf32f64
        })
        const cube = new THREE.Mesh(geometry, material)
        // 没有添加光源之前 受光照影响的材料都会只是黑色的存在
        scene.add(cube)
        // 创建一个点光源 ( 点光源就是模拟一个灯泡发出的光源)
        // PointLight( 光的颜色,光的强度 )
        const pointLight = new THREE.PointLight(0xffffff, 1.0)
        //  pointLight.position.set(x轴,y轴,z轴) 设置光源的位置
        // pointLight.position.set( 10,0,0);// v设置光源在x轴上
        pointLight.position.set(100, 6, 6);
        // 在场景当中添加光源
        scene.add(pointLight)
        // 为了方便我们看到我们的光源的位置  threejs提供了一个helper帮我们查看光源的位置
        const pointLightHelper = new THREE.PointLightHelper(pointLight, 20);
        scene.add(pointLightHelper);
        camera.position.z = 5;
        camera.position.x = 2
        // camera.position.y = 5;
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);// 在整个场景当中添加坐标辅助线
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', () => {
            renderer.render(scene, camera)
        })
        renderer.render(scene, camera)
    </script>
</body>

</html>

GUI 的使用

由于3d对于很多新手而已空间位置跟传统的网页有很大的不同,官方文档提供的各个api有时候又有特别多的参数需要设置,对于新手特别的不友好,所以官方提供了GUI的这个工具允许我们在直观的对于参数进行动态的设置和效果的预览。如同下图一样

three.js docs

引入 GUI 

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
// 实例化一个gui对象
const gui = new GUI();

.add()方法

执行gui的.add()方法可以快速创建一个UI交互界面,比如一个拖动条,可以用来改变一个JavaScript对象属性的属性值。
格式:.add(控制对象,对象具体属性,其他参数)
其他参数,可以一个或多个,数据类型也可以不同,gui会自动根据参数形式,自动生成对应的交互界面。
参数3和参数4,分别是一个数字,交互界面是一个鼠标可以拖动的拖动条,可以在一个区间改变属性的值
执行gui.add(obj, 'x', 0, 100);你可以发现右上角gui界面增加了新的内容,可以控制obj对象x属性的新交互界面。

//创建一个对象,对象属性的值可以被GUI库创建的交互界面改变
const obj = {
  x: 30,
};
// gui增加交互界面,用来改变obj对应属性
gui.add(obj, 'x', 0, 100);

体验.add()功能——改变对象属性值

为了方便观察.add()是如何改变JavaScript对象属性的,可以浏览器控制台不停地打印obj的值,这样通过gui界面拖动改变obj对象属性的的时候,便于观察obj的变化。
 

const obj = {x: 30};
gui.add(obj, 'x', 0, 100);
setInterval(function () {
  console.log('x', obj.x);
}, 10)

分组 addFolder()

当GUI交互界面需要控制的属性比较多的时候,为了避免混合,可以适当分组管理,这样更清晰

分组前

gui.add( originPositon,'x', 0,20).onChange(val=>{
            cube.position.x = val;
            renderer.render(scene, camera)   
})
 gui.add( originPositon,'y', 0,20).onChange(val=>{
            cube.position.y = val;
            renderer.render(scene, camera) 
})
gui.add(cameraPosition,'x',-5,15 ).onChange(val=>{
            camera.position.x = val;
            renderer.render(scene, camera) 
})

分组后 

// 创建第一个 gui的参数分组   几何体位置
const cubeParams = gui.addFolder('几何体位置');
cubeParams.add(originPositon, 'x', 0, 20).onChange(val => {
    cube.position.x = val;
    renderer.render(scene, camera)
})
cubeParams.add(originPositon, 'y', 0, 20).onChange(val => {
    cube.position.y = val;
    renderer.render(scene, camera)
})
//  相机位置的分组
const cameraParams = gui.addFolder('相机的位置')
cameraParams.add(cameraPosition, 'x', -5, 15).onChange(val => {
    camera.position.x = val;
    renderer.render(scene, camera)
})
renderer.render(scene, camera) 
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        // three内置了 gui工具 我们先进行一个导入 允许我们动态的对于一些参数进行设置方便我们预览效果
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({
            // alpha: true, // 背景是否可以设置透明色
            antialias: true, //表示是否开启反锯齿
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({
            color: 0xff0000
        })
        const cube = new THREE.Mesh(geometry, material)
        scene.add(cube)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);// 在整个场景当中添加坐标辅助线
        camera.position.z = 5;
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', () => {
            renderer.render(scene, camera)
        })
        // 创建一个GUI实例
        const gui = new GUI();
        // cube的位置
        const originPositon = {
            x: 0,
            y: 0,
            z: 0
        }
        // camera的位置
        const cameraPosition = {
            x: 0,
            y: 0,
            z: 5
        }
        // 创建第一个 gui的参数分组   几何体位置
        const cubeParams = gui.addFolder('几何体位置');
        cubeParams.add(originPositon, 'x', 0, 20).onChange(val => {
            cube.position.x = val;
            renderer.render(scene, camera)

        })
        cubeParams.add(originPositon, 'y', 0, 20).onChange(val => {
            cube.position.y = val;
            renderer.render(scene, camera)
        })
        //  相机位置的分组
        const cameraParams = gui.addFolder('相机的位置')
        // add(对象,属性名,数值)  生成的是一个数字输入框的交互
        // add(对象,属性名,数值1,数值2) 生成的是一个有可选范围的数字选择框
        cameraParams.add(cameraPosition, 'x', -5, 5).onChange(val => {
            camera.position.x = val;
            renderer.render(scene, camera)
        })
        // add(对象,属性名,数组)  会自动生成一个下拉菜单的交互界面
        //   cameraParams.add(cameraPosition,'x' ,[1,3,6]).onChange(val=>{
        //     camera.position.x = val;
        //     renderer.render(scene, camera) 
        //  })
        // add( 对象,属性名, 对象 ) 也会生成一下下拉菜单的交互方式
        // cameraParams.add(cameraPosition,'x' ,{
        //     param1: 1,
        //     param2: 5
        // }).onChange(val=>{
        //     camera.position.x = val;
        //     renderer.render(scene, camera) 
        //  })
        const testObj = {
            color: 0xff6600,
            val1: 10
        }
        // addColor 在gui当中添加一个 颜色选择器的交互 
        gui.addColor(testObj, 'color').onChange(val => {
            console.log('颜色的值', val)
        })
        // add()的其他后续的方法
        // .name(直接展示为参数的指定名称 方便理解)
        // .step(步进的数量) 每一个变化的最小单位
        gui.add(testObj, 'val1', 0, 100).name('某个值的大小').step(0.5)
        renderer.render(scene, camera)
    </script>
</body>

</html>

相机的理解

查看 Three.js 的文档,可以看到 Camera 是一个抽象类,一般不直接使用,其他类型的 Camera 实现了这个抽象类。有以下的几种相机

  • ArrayCamera 包含着一组子摄像机,常用于多人同屏的渲染,更好地提升VR场景的渲染性能
  • StereoCamera 双透视摄像机(立体相机),常用于创建 3D 立体影像,比如 3D 电影之类或 VR
  • CubeCamera 有6个渲染,分别是立方体的6个面,常用于渲染环境、反光等
  • OrthographicCamera 正交相机,在这种投影模式下,无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变。这对于渲染2D场景或者UI元素是非常有用的。
  • PerspectiveCamera 透视相机,这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式。

接下来我们来重点了解一下 透视投影相机和正投影相机

透视投影相机PerspectiveCamera

透视投影相机PerspectiveCamera本质上就是在模拟人眼观察这个世界的规律。

透视指的其实就是远小近大的效果,所以透视投影是threejs当中使用最普遍的相机模式。

// 实例化一个透视投影相机对象
const camera = new THREE.PerspectiveCamera();

PerspectiveCamera参数介绍

PerspectiveCamera( fov, aspect, near, far )

参数

含义

默认值

fov

相机视锥体竖直方向视野角度

50

aspect

相机视锥体水平方向和竖直方向长度比,一般设置为Canvas画布宽高比width / height

1

near

相机视锥体近裁截面相对相机距离

0.1

far

相机视锥体远裁截面相对相机距离,far-near构成了视锥体高度方向

2000

//相机在Three.js三维坐标系中的位置
// 根据需要设置相机位置具体值
camera.position.set(200, 200, 200);

相机位置.position

生活中用相机拍照,你相机位置不同,拍照结果也不同,threejs中虚拟相机同样如此。

比如有一间房子,你拿着相机站在房间里面,看到的是房间内部,站在房子外面看到的是房子外面效果。

相机对象Camera具有位置属性.position,通过位置属性.position可以设置相机的位置。

相机观察目标.lookAt()

你用相机拍照你需要控制相机的拍照目标,具体说相机镜头对准哪个物体或说哪个坐标。对于threejs相机而言,就是设置.lookAt()方法的参数,指定一个3D坐标。

//相机观察目标指向Threejs 3D空间中某个位置
camera.lookAt(0, 0, 0); //坐标原点
camera.lookAt(0, 10, 0);  //y轴上位置10
camera.lookAt(mesh.position);//指向mesh对应的位置
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({
            color: 0xff0000
        });
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);
        // 创建了一个透视相机实例
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 5;
        //  camera.position.x = 5;
        //  camera.position.y = 5;
        // 如同我们平时拍照一样 我们可以让我们的相机对准某个方向
        //  camera.lookAt(cube.position); // 把相机对准某个模型的中心
        // camera.lookAt(8,0,0,)
        const gui = new GUI();
        gui.add(camera.position, 'x', 1, 50).name('相机的位置x轴的位移')
        gui.add(camera.position, 'y', 1, 50).name('相机的位置y轴的位移')
        gui.add(camera.position, 'z', 1, 50).name('相机的位置z轴的位移')
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        let angle = 0; // 用于计算圆周运动的角度值 
        const R = 40; //  圆周远动的半径
        camera.position.y = 50
        function animate() {
            requestAnimationFrame(animate);
            // camera.position.x +=0.1
            // angle +=0.01;
            // camera.position.x = R * Math.cos(angle);
            // camera.position.z = R * Math.sin(angle); 
            camera.lookAt(cube.position); // 还是需要手动的去设置 lookAt看向物体的中心
            renderer.render(scene, camera)
        }
        animate(); 
    </script>
</body>

</html>

正投影相机OrthographicCamera

正投影相机的长方体可视化空间和透视投影PerspectiveCamera视锥体相似,只是形状不同。

正投影相机的参数

// 构造函数格式
OrthographicCamera( left, right, top, bottom, near, far )

参数(属性)

含义

left

渲染空间的左边界

right

渲染空间的右边界

top

渲染空间的上边界

bottom

渲染空间的下边界

near

near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1

far

far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值2000

正投影相机和透视投影相机区别

一句话描述,正投影相机OrthographicCamera和透视投影相机PerspectiveCamera的区别就是,透视投影可以模拟人眼观察世界的视觉效果,正投影相机不会。

对比透视投影和正投影预览工厂视觉差异

比如前面工厂的gltf模型加载案例,就是用透视投影相机模拟人在空中俯视地面的效果,如果使用正投影相机渲染效果就不太自然。

相机选择

对于大部分需要模拟人眼观察效果的场景,需要使用透视投影相机,比如人在场景中漫游,或是在高处俯瞰整个园区或工厂。

正投影没有透视效果,也就是不会模拟人眼观察世界的效果。在一些不需要透视的场景你可以选择使用正投影相机,比如整体预览一个中国地图的效果,或者一个2D可视化的效果

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }

    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({
            color: 0xff0000
        });
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);
        const s = 5; // 预设一个范围
        const k = window.innerWidth / window.innerHeight; // 视图的长宽比(canvas画布的长宽比)
        const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 0.1, 2000)
        camera.position.z = 1
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate(); 
    </script>
</body>

</html>

几何体的理解

学习threejs绕不开对于几何体的深入了解,所以新手需要对几何体的构建原理有简单的理解,这样才能更方便进行后续的学习。

BufferGeometry

缓冲类型几何体对象BufferGeometry 可以理解为绝大多数几何体的本质BufferGeometry 可以先简单的理解为通过定义顶点来描述几何体的外形。接下来我通过点模型和线模型网格模型来逐步理解缓冲模型。

点模型

点模型就是在3d空间当中添加一个点。

先创建一个缓冲几何体

const geometry = new THREE.BufferGeometry();

这个时候缓冲几何体还只是一个抽象的存在,没有具体的形状的表示。

接下来使用 类型数组 创建一个顶点坐标的数组,这些顶点数据就是最终点的位置。

类型数组 Float32Array:

Float32Array - JavaScript | MDN

   const vertices = new Float32Array([
            0,0,0, // 第1个点的位置
            3,0,0, // 第2个点的位置
            0,3,0, // 第3个点的位置
            0,0,3, // 第4个点的位置
           ....    // 你还可以设置更多的点
   ])

将数组的信息转换成顶点数据属性 然后修改缓冲几何体的属性
 

// 通过 BufferAttribute来创建几何体的具体的顶点数据
const attribue = new THREE.BufferAttribute(vertices , 3); // 3 表示每3个数值为一组
// 设置缓冲几何体的顶点位置
geometry.attributes.position = attribue;

经过了这一步 缓冲几何体 就有了具体的点的坐标属性

如何Mesh网格模型有 网格模型的材料一样 点模型也有自己的点模型材料 PointsMaterial

  // 点模型有点模型自己的材料方法
const meterial = new THREE.PointsMaterial({
  color:0xff6600,
  size: 0.1, //设置点的大小
})

创建点模型的方法跟创建网格模型的方式基本是一样的,通过 THREE.Points 就可以创建点模型

// Mesh创建的是网格模型  Points创建的就是 点模型
const point = new THREE.Points(geometry,meterial);
scene.add(point);  // 把点模型 添加到场景中

场景当中就有了我们刚才设置的三个点。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>

    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }

    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        // BufferGeometry 缓存几何体  可以自定义的几何体  
        // 创建一个几何体的本质就是 设置几何体的各个顶点
        const geometry = new THREE.BufferGeometry();
        // 创建几何体的 顶点数据
        const vertices = new Float32Array([
            0, 0, 0, // 第一个点的位置
            3, 0, 0,
            0, 3, 0,
            0, 0, 3
        ])
        // 通过 BufferAttribute来创建几何体的具体的顶点数据
        const attribue = new THREE.BufferAttribute(vertices, 3);
        // 设置缓冲几何体的顶点位置
        geometry.attributes.position = attribue;
        // 点模型有点模型自己的材料方法
        const meterial = new THREE.PointsMaterial({
            color: 0xff6600,
            size: 0.1, //设置点的大小
        })
        // Mesh创建的是网格模型  Points创建的就是 点模型
        const point = new THREE.Points(geometry, meterial)
        scene.add(point);
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 5
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate(); 
    </script>
</body>

</html>

线模型

线模型,就是在空间当中设置一些连线的模型。它的视线跟点模型有一定的联系,先有点然后才有线。

从创建缓冲几何体和设置顶点数据的流程是一样的。

const geometry = new THREE.BufferGeometry();
// 创建几何体的 顶点数据
const vertices = new Float32Array([
    0,0,0, // 第一个点的位置
    3,0,0,
    0,3,0,
    0,0,3
])
// 通过 BufferAttribute来创建几何体的具体的顶点数据
const attribue = new THREE.BufferAttribute(vertices , 3);
geometry.attributes.position = attribue;

线模型 自然也会有线模型的材料

 // 改用line模型的材料
const meterial = new THREE.LineBasicMaterial({
   color:0xff6600,
   linewidth:1,
   linecap: 'round', //ignored by WebGLRenderer
   linejoin:  'round' //ignored by WebGLRenderer
})

使用 Line方法来创建 线模型

    const line = new THREE.Line(geometry,meterial); // 普通的线

创建线模型还有其他的两个方法  LineLoop LineSegments  

// const line = new THREE.LineLoop(geometry,meterial); // 闭合的线
// const line = new THREE.LineSegments(geometry,meterial); // 非连续的线
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>线模型</title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 5
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        const geometry = new THREE.BufferGeometry();
        // 创建几何体的 顶点数据
        const vertices = new Float32Array([
            0, 0, 0, // 第一个点的位置
            3, 0, 0,
            0, 3, 0,
            0, 0, 3
        ])
        // 通过 BufferAttribute来创建几何体的具体的顶点数据
        const attribue = new THREE.BufferAttribute(vertices, 3);
        geometry.attributes.position = attribue;
        // 改用line模型的材料
        const meterial = new THREE.LineBasicMaterial({
            color: 0xff6600,
            linewidth: 1,
            linecap: 'round', //ignored by WebGLRenderer
            linejoin: 'round' //ignored by WebGLRenderer
        })
        // Mesh创建的是网格模型  Points创建的就是 点模型
        // 绘制线模型的方法有三个 Line  LineLoop LineSegments  
        const line = new THREE.Line(geometry, meterial); // 普通的线
        // const line = new THREE.LineLoop(geometry,meterial); // 闭合的线
        // const line = new THREE.LineSegments(geometry,meterial); // 非连续的线
        scene.add(line)
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

网格模型

经过了点模型和线模型的学习了解,我们再回来看一下我们最熟悉的 线网格模型。 我们在场景当中添加一个普通的 网格模型。 在网格材质的地方我们设置一个新的属性

wireframe 为 true,wireframe的配置是开启展示模型的顶点和网线数据。

网格模型的父类都是 BufferGeometry ,内置的几何体( Box Plane 等等)本质上就是threejs计算好了之后的和封装相关属性的BufferGeometry几何体。

const geometry = new THREE.BoxGeometry(1,1,1);
const material = new THREE.MeshBasicMaterial({
    color: 0xff6600,
    wireframe:true, // 用线模型的模式展示mesh对应的三角形
})
const cube = new THREE.Mesh(geometry,material );
scene.add(cube);

我们可以看到 网格几何体的表面是由多个三角形组合而成。其实我们通过官方的 Geometry的展示当中也可以看到基本所有的几何体都是由各个三角形组合而成。每个几何体基本都有一个 segments 效果的配置,这个值越大那么三角形细分的越多,就是表示就是几何体的表面越来越细节,越平滑,但是往往也会带来更大的性能的消耗

所以我们在一些3d模型的下载网站,在描述一个3d模型的会说明三角形的数量和顶点数据的数量。

所以不影响效果的前提下,我们尽量选择顶点数据更少的几何体。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>网格模型</title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        // 第一眼看到的位置
        camera.position.z = 5
        camera.position.x = 5
        camera.position.y = 5
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        // 实际上我们的 Mesh网格模型 无论是是 BoxGeometry 还是其他的几何体都好 本质上都是有一个个三角形平面构建而成
        // 只是使用  BoxGeometry CapsuleGeometry 等等的这些内置的方法 会帮我们计算好几何体的顶点数据
        // 在不影响 展示效果的提前之下,我们的顶点数据越少 性能越高 
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({
            color: 0xff6600,
            wireframe: true, // 用线模型的模式展示mesh对应的三角形
        })
        const cube = new THREE.Mesh(geometry, material);
        // 改变物体的位置
        cube.position.x = 1
        cube.position.y = 2
        cube.rotateX(Math.PI / 4); // 旋转
        scene.add(cube);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

几何体的一些公共方法

threejs 对于所有的BufferGeometry的几何体都提供了一些公共的方法。下面挑几个最常用的来进行学习。

对于学习过css当中的transform的同学就会比较容易理解。

缩放

 geometry.scale(2,2,1); // 几何体在xyz三个方向上的缩放倍数

位移

geometry.translate(3,1,1)
// 居中
 geometry.translate(3,1,1)
		setTimeout(()=>{
       // center方法 会让几何体居中对齐我们的坐标原点
        geometry.center()
},2000)

旋转

 geometry.rotateX( Math.PI / 4 ); // x轴 4分之一圈的弧度  45deg
 // geometry.rotateY( Math.PI / 4 ); // y轴
 // geometry.rotateZ( Math.PI / 4 ); //z轴

模型对象和基础材质

几何体并不是最终展示在场景当中看得见的物体,所有的几何体都需要经过对应的创建模型的方法才能称为最终要展示的物体,这些物体我们有更专业的叫法:模型对象。 我们创建模型对象的时候往往还需要伴随设置模型的材质。

通过 THREE.Points THREE.Line THREE.Meash 分别可以创建 点模型 模型 和网格模型。创建出来的模型都有一些公共的方法和属性,我们接下来学习一下一些方法和属性

模型的位置和缩放

在场景当中我们新建的模型默认是出现在坐标原点,但是随着场景当中的模型的增加,各个模型的摆放的位置肯定是有所区别的,所以需要学会如何调整模型的位置。

了解的位置的摆放我们需要学习一个对象 Vector3 三维向量, 他表达的是一个拥有 x y z 三个属性的对象,你可以理解为表示一个三维空间的坐标的值。

const v3  = new THREE.Vector3(0,0,0);
// Vector3 对象都可以调用 set方法来修改具体的值
v3.set(5,0,9);
console.log('v3 after',v3);
v3.y = 9;
console.log('v3的属性直接修改之后',v3)

经过上面的学习你就知道了 一个 Vector3类型的对象 可以调用set方法来修改它的值,也可以像普通对象一样直接修改它的属性的值。

通过console.log我们可以看到 模型对象的position属性就是一个 Vector3对象。所以我们可以按照以下的方式修改它的位置。( tips: threejs当中的很多实例的position属性都是 Vector3对象 )

model1.position.set(2,0,0)
model1.position.x  = 2 // 跟上面的一行代码是等价的

除了修改模型的position属性以外,模型对象自身也有三个方向对应的平移方法

 // 所有的模型对象都具有 平移的方法
  model1.translateX(2);
  model1.translateY(2);
  model1.translateZ(2);
// 等价于  model1.position.set(2,2,2)

模型的角度

实际开发当中我们除了要移动模型的以外,我们可能还需要对于模型的角度进行调整,这个时候就涉及到模型的角度设置的三个方法。

在执行旋转之前我们先要对threejs当中的角度数值进行了解

 // Math.PI 常亮来代替  180°
 // Math.PI / 2  => 90°
 // Math.PI / 4  => 45°

了解了角度的取值之后我们就可以尝试使用以下的方法来对模型对象进行旋转操作

model1.rotateX(Math.PI / 4); 
model1.rotateY(Math.PI / 4)
model1.rotateZ(Math.PI / 4)

材质颜色

在这里我们主要还是讲解如何给我们的材质设置不同的颜色

我们在创建材质的时候一般就会对材质的颜色进行设置

 const material = new THREE.MeshBasicMaterial({
       color: 0xff6600, // 官方的文档当中都是采用16进制的方式来设置一个颜色
})

但是如果后期需要对于材质的进行进行修改,我们可以通过对材质对象的color属性进行set的操作来实现

// material.color.setRGB(0,135,150) // 通过设置RGB的值来设置一个指定的颜色(这里采用的rgb值不是0-255 而是百分比)
// material.color.setStyle('#008796') ;// 使用我们css的样式当中的常用的颜色的表达形式
// 如果我们希望采用的是 0-255的数值来表达 RGB的值的话我们要采用下面的写法
// material.color.set('rgb(161,105,0)')
// 使用set方法也可以设置为 setStyle的参数
// material.color.set('#ff0000')
material.color.set('blue')

模型的几何体属性和材料属性

在我们学习了第9节的几何体和上面的材质颜色的设置之后,我们通过模型对象也可以得到这个模型对象对应的 几何体属性和材料属性。 意思就是我们可以对于模型对象的 geometry 属性 和 material 属性进行操作,从而改变渲染效果。

// 创建一个具体的网格模型
const model1 = new THREE.Mesh(geometry, material);
scene.add(model1);
console.log('网格模型', model1) // 通过打印我们可以看到在 模型对象上我们有具体的集合体对象和材料对象
// 我们也可以通过 模型文件去操作 对应的集合体和材料
model1.material.color.set('#ff6600') // 通过 模型的material属性可以操作材料
model1.geometry.translate(2, 0, 0); // 通过 模型的geometry属性可以调用 geometry的一些方法

模型分组

如果我们进行的是一些中大型的3d场景开发,往往有很多的几何体呈现在场景当中,但是有一些几何体又是有多个几何体构成的,比如说一辆车可以简单理解为 一个盒形几何体+4个轮状几何体结合而成,在大的场景中我们希望把这个 1+4的组合当成一个整体。这种场景我们就需要对于我们的模型进行分组。

Goup 新建分组

先新建两个模型

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({
    color: 0xff6600
})
const model1 = new THREE.Mesh(geometry, material)
const model2 = new THREE.Mesh(geometry, material)
model2.position.x = 3;// model2其实是沿着x轴平移了2个单位(如果有分组的话 那么其实是相对于局部坐标移动)

然后通过 Group 方法来新建一个分组

const group1 = new THREE.Group(); // 创建一个分组 group1

add方法 添加模型

把两个模型添加到新的分组当中

// 添加内容到分组当中
// 把model1 和model1组合在一起形成一个分组大的模型
group1.add(model1);
group1.add(model2);
// 也可以一次添加多个  group1.add(model1,model2)

尝试移动整个分组

// 分组本质上就多个小模型组成的一个大的模型 所以也可以调用模型对象的公共的方法
group1.translateY(2);
group1.translateX(2); 

name 命名

当我们考虑使用分组意味着我们的场景当中放置的模型会越来越多,所以如果通过js代码来找到我们要的指定的模型就会成为一个常见的需求,如图原生js的dom的操作一样,threejs也提供了方法来方便我们获取指定的模型,getObjectByName() 方法通过通过传入一个模型的name属性的值就可以快速的得到指定的模型对象。所以我们一般会给我们的模型对象设置一个name属性。

const maaterial2 = new THREE.MeshBasicMaterial()
const model3 = new THREE.Mesh(geometry, maaterial2);
const model4 = new THREE.Mesh(geometry, maaterial2);
model3.position.z = 2;
model4.position.z = 4
model3.name = '3号';
model4.name = '4号';
const group2 = new THREE.Group(); // 创建一个分组 group2
group1.name = '第二组'
group2.add(model3);
group2.add(model4);
group2.position.x = 3;// 也可以通过position来设置分组的位置
scene.add(group2)
// 通过scene的children可以看到这个的场景的对象树的结构
console.log('场景的全部子内容', scene.children)
// 原生js可以通过 getElmentById来获取指定id的节点
// threejs当中 提供了有一个方法 来帮我们通过name属性来寻找指定的模型
const no4 = scene.getObjectByName('4号');
console.log('no4', no4)
no4.rotateX(Math.PI / 4)

通过 getObjectByName 我们就可以快速的获取我们得到的指定的模型对象

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>模型分组</title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }

    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({
            color: 0xff6600
        })
        const model1 = new THREE.Mesh(geometry, material)
        const model2 = new THREE.Mesh(geometry, material)
        model1.name = '1号';
        model2.name = '2号';
        model2.position.x = 3;// model2其实是沿着x轴平移了2个单位(如果有分组的话 那么其实是相对于局部坐标移动)
        const group1 = new THREE.Group(); // 创建一个分组 group1
        group1.name = '第一组'
        // 添加内容到分组当中
        // 把model1 和model1组合在一起形成一个分组大的模型
        group1.add(model1);
        group1.add(model2);
        // 分组本质上就多个小模型组成的一个大的模型 所以也可以调用模型对象的公共的方法
        group1.translateY(2);
        group1.translateX(2);
        // 相对于场景当中的原点  model2 自己身x轴移动了3个三位, 所在的分组x轴也移动了2个单位
        // 运行结果当中可以知道 我们的model2 对于我们的整个原始坐标实际移动了3+2(x轴)
        // 世界坐标  和 局部(本地)坐标
        // 世界坐标指的就是  scene当中的坐标 就是所有的人都要参考的坐标
        // 局部坐标就是分组内部的坐标
        // 可以通过添加分组内的坐标辅助线来理解
        const group1AxesHelper = new THREE.AxesHelper(10);
        group1.add(group1AxesHelper)
        // 在场景当中 添加分组
        scene.add(group1)
        const maaterial2 = new THREE.MeshBasicMaterial()
        const model3 = new THREE.Mesh(geometry, maaterial2);
        const model4 = new THREE.Mesh(geometry, maaterial2);
        model3.position.z = 2;
        model4.position.z = 4
        model3.name = '3号';
        model4.name = '4号';
        const group2 = new THREE.Group(); // 创建一个分组 group2
        group1.name = '第二组'
        group2.add(model3);
        group2.add(model4);
        group2.position.x = 3;// 也可以通过position来设置分组的位置
        scene.add(group2)
        // 通过scene的children可以看到这个的场景的对象树的结构
        console.log('场景的全部子内容', scene.children)
        // 原生js可以通过 getElmentById来获取指定id的节点
        // threejs当中 提供了有一个方法 来帮我们通过name属性来寻找指定的模型
        const no4 = scene.getObjectByName('4号');
        console.log('no4', no4)
        no4.rotateX(Math.PI / 4)
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 7
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate(); 
    </script>
</body>

</html>

网格纹理贴图

之前我们所学的内容当中我们使用的材料都是仅仅设置了颜色,开发当中我们的3d模型呈现的却是更接近现实当中的各色各样的效果,这节课我们就来学习一下如何给模型文件使用纹理贴图的材料。

使用贴图的流程

新建一个纹理加载器

 const textLoader = new THREE.TextureLoader(); // 创建纹理加载器

加载一个图片来作为纹理对象
可以通过以下的网站来获取纹理贴图
Free textures and tutorials for Photoshop and more!
Transparent Textures
Poly Haven
 

const texture =  textLoader.load('../assets/floor.jpg');  // 通过加载一个图片来得到一个纹理

创建材料的时候设置的就不是color属性了 而是使用map来使用上一步创建好的纹理对象

side : THREE.DoubleSide 表示双面可见,因为默认情况下几何体之后一面是贴图可见的

const material = new THREE.MeshBasicMaterial({
  // color: 0xd3d3d3, 
  map: texture, // 使用指定的纹理对象来渲染我们的模型
  side: THREE.DoubleSide
})

使用尝试使用采用了贴图的材料

const floor = new THREE.Mesh(geometry,material )
loor.rotateX( Math.PI /2 ); // 平放
scene.add(floor)

你可以创建其他的几何体模型来体验贴图的效果

贴图阵列(平铺)

根据上面的代码运行的效果我们可以知道 默认情况下threejs会用一整张图去贴在材质的表面,但是有些时候我们需要的是在一个面上使用一个图片平铺展示的效果,比如说地毯瓷砖等。这个之后我们就需要使用到阵列来实现。

// 设置阵列模式
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(5,5);//注意选择合适的阵列数量

设置了阵列模式,那么贴图会按照指定的大小数量平铺在我们的模型上。

透明贴图

有些时候我们会在我们的场景当中使用写透明的贴图,比如说路牌指示,或者是标签效果等。这个时候使用plane几何体和透明图片的组合时候就会特别适合。

// 新建一个路牌
const lpgeometry = new THREE.PlaneGeometry(1, 1);
const lpmaterial = new THREE.MeshBasicMaterial({
    map: textLoader.load('../assets/指示牌.png'),
    side: THREE.DoubleSide,
    transparent: true, //如果需要保持贴图的透明性  需要开启透明属性
})
const lp = new THREE.Mesh(lpgeometry, lpmaterial);
lp.position.y = 2;
scene.add(lp)

特别需要注意的是 在创建材料的时候一定要记得设置 材料的透明属性的开启 transparent: true

不然 png 图片透明的部分就会显示为黑色。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>纹理贴图</title>
</head>

<body>

    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }

    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.PlaneGeometry(50, 50);
        // threejs的材料除了可以设置颜色以外 还可以通过贴图的形式来实现多样的纹理效果
        const textLoader = new THREE.TextureLoader(); // 创建纹理加载器
        const texture = textLoader.load('../assets/300.png');  // 通过加载一个图片来得到一个纹理
        // 设置阵列模式
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(15, 15);//注意选择合适的阵列数量
        const material = new THREE.MeshBasicMaterial({
            // color: 0xd3d3d3, 
            map: texture, // 使用指定的纹理对象来渲染我们的模型
            side: THREE.DoubleSide
        })
        // 默认情况下 平面模型只能看到一面 如果需要两面都能看到的话需要设置材料的side属性
        const floor = new THREE.Mesh(geometry, material)
        floor.rotateX(Math.PI / 2); // 平放
        scene.add(floor)
        // 创建一个 盒子
        const newBox = new THREE.BoxGeometry(1, 1, 1);
        const boxOut = textLoader.load('../assets/300.png')
        const iron = new THREE.MeshBasicMaterial({
            map: boxOut,
        })
        const ironBox = new THREE.Mesh(newBox, iron);
        ironBox.position.x = 2;
        ironBox.position.y = 3;
        scene.add(ironBox)
        // 新建一个路牌
        const lpgeometry = new THREE.PlaneGeometry(1, 1);
        const lpmaterial = new THREE.MeshBasicMaterial({
            map: textLoader.load('../assets/指示牌.png'),
            side: THREE.DoubleSide,
            transparent: true, //如果需要保持贴图的透明性  需要开启透明属性
        })
        const lp = new THREE.Mesh(lpgeometry, lpmaterial);
        lp.position.y = .5;
        scene.add(lp)
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 100;
        camera.position.x = 100;
        camera.position.y = 100
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

加载外部的3d模型

大家经常在看到别人做的three项目当中有很多的各式各样的3d模型,有车辆房子动物等。这些模型也许对于新手完全的0到1去通过threejs的几何体去创建其实是不太现实的,很多时候我们的3d模型是使用c4d 或者是 blender这一类的3d建模设计软件去设计的,3d软件完成之后就可以导出对应的模型文件,而我们使用这些文件加载到我们的场景中就可以使用了。

可以免费下载到3d模型的网站:
Sketchfab - The best 3D viewer on the web
Poly Haven
http://gltfs.com/

如何加载外部的3d模型 :

GLTF文件的加载

现在顶部引入 GLTFLoader

// 引入gltf模型加载库GLTFLoader.js
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

新建一个 GLTF 加载器

 const loader = new GLTFLoader(); // 创建一个gltf文件加载器

加载一个gltf文件 然后添加到场景当中

gltf 是threejs当中常用的模型文件格式,它的地位就如何jpg图片在网页当中的地位一样

gltf模型下载的时候有的是单独的打包贴图(texture)和bin文件,复制的时候可不要忘记相关的文件

loader.load('../assets/sofa/scene.gltf', function (gltf) {
    const sofa = gltf.scene
    // gltf的.scene就是我们最终可以添加到 场景当中的模型j
    gltf.scene.scale.x = 2;
    gltf.scene.scale.y = 2;
    gltf.scene.scale.z = 2;
    sofa.position.x = 2
    scene.add(gltf.scene)
})

在threejs当中 大小只有数值没有单位,我们的导入的模型的大小我们需要通过 scale来进行缩放处理调整大小

GLB文件的加载

glb是也是我们的threejs当中常用的3d模型文件,跟gltf的一个很大的区别就在于glb都只是只有一个单独的文件,没有贴图文件的额外的打包。不过它的使用流程跟gltf是一样的

//  加载外部的 glb格式的3d模型  加载的方式跟gltf格式的文件是一样的
loader.load('../assets/old_soviet_chair.glb', function (res) {
    const chair = res.scene;
    // 默认出现在原点 所以需要进行一定的位置的移动
    chair.position.set(-2, 0, 0)
    cene.add(chair)
})
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title>纹理贴图</title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        // 引入gltf模型加载库GLTFLoader.js
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        const geometry = new THREE.PlaneGeometry(50, 50);
        // threejs的材料除了可以设置颜色以外 还可以通过贴图的形式来实现多样的纹理效果
        const textLoader = new THREE.TextureLoader(); // 创建纹理加载器
        const texture = textLoader.load('../assets/1.png');  // 通过加载一个图片来得到一个纹理
        // 设置阵列模式
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(5, 5);//注意选择合适的阵列数量
        const material = new THREE.MeshBasicMaterial({
            // color: 0xd3d3d3, 
            map: texture, // 使用指定的纹理对象来渲染我们的模型
            side: THREE.DoubleSide
        })
        // 默认情况下 平面模型只能看到一面 如果需要两面都能看到的话需要设置材料的side属性
        const floor = new THREE.Mesh(geometry, material)
        floor.rotateX(Math.PI / 2); // 平放
        scene.add(floor)
        // 新建一个路牌
        const lpgeometry = new THREE.PlaneGeometry(1, 1);
        const lpmaterial = new THREE.MeshBasicMaterial({
            map: textLoader.load('../assets/指示牌.png'),
            side: THREE.DoubleSide,
            transparent: true, //如果需要保持贴图的透明性  需要开启透明属性
        })
        const lp = new THREE.Mesh(lpgeometry, lpmaterial);
        lp.position.y = 2;
        scene.add(lp)
        // 加载一个 gltf的外部的3d模型
        // gltf等同于3d世界当中的 jpg图片 
        const loader = new GLTFLoader(); // 创建一个gltf文件加载器
        loader.load('../assets/sofa/scene.gltf', function (gltf) {

            const sofa = gltf.scene
            // gltf的.scene就是我们最终可以添加到 场景当中的模型j
            gltf.scene.scale.x = 2;
            gltf.scene.scale.y = 2;
            gltf.scene.scale.z = 2;
            sofa.position.x = 2
            // scene.add(gltf.scene)
        })
        loader.load('../assets/duck/rubber_duck_toy_4k.gltf/rubber_duck_toy_4k.gltf', (gltf) => {
            const duck = gltf.scene;
            duck.scale.x = 3;
            duck.scale.y = 3;
            duck.scale.z = 3;
            scene.add(duck)
        })
        loader.load('../assets/aircon/exterior_aircon_unit_1k.gltf', (gltf) => {
            const aircon = gltf.scene;
            aircon.scale.x = 5;
            aircon.scale.y = 5;
            aircon.scale.z = - 10;
            scene.add(aircon)
        })
        //  加载外部的 glb格式的3d模型  加载的方式跟gltf格式的文件是一样的
        loader.load('../assets/old_soviet_chair.glb', function (res) {
            const chair = res.scene;
            // 默认出现在原点 所以需要进行一定的位置的移动
            chair.position.set(-2, 0, 0)
            scene.add(chair)
        })
        const pointLight = new THREE.PointLight(0xffffff, 1.0)
        pointLight.position.set(100, 60, 50);
        scene.add(pointLight);
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 50;
        camera.position.x = 50;
        camera.position.y = 50
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

PBR材质

我们在3d场景当中我们加载了一个仿真的环境场景,而我们的3D模型放置在这样的环境当中如果需要呈现更接近于现实的场景的话就得使用PBR材质,PBR材质的概念是我们通过threejs构建出更符合物理特性(反光,折射)的材料。

PBR 材质主要采用的是 MeshStandardMaterial 和 MeshPhysicalMaterial 来构建。我们通过使14节课的环境场景来学习PBR材质的使用

我们先新建球形的几何体

const geometry = new THREE.SphereGeometry( 1, 32, 16 ); 

金属材质

然后我们通过 MeshStandardMaterial 来构建一个PBR材质

metalness 金属度 取值范围 0-1 0 完全非金属 1完全的金属

roughness 材质表面的粗糙程度 0 完全光滑 1完全漫反射(极度不平整)

envMap 环境材料 ,取值为一个环境贴图,表示物体的表面的内容会结合当前的所在的周边环境(倒影反射等)

envMapIntensity: 材料跟环境的结合程度 1 完全贴合

     const  material = new THREE.MeshStandardMaterial({
            metalness: 1.0, // 金属度
            roughness: 0, // 粗糙程度 1 表示漫反射 0完全光滑(镜面效果更明显)
            envMap: cubeTexture,  //给材料也设置一个指定的环境贴图
            envMapIntensity: 1 
        })

跟以往一样新建网格模型并添加到场景当中

const ball = new THREE.Mesh(geometry, material); 
scene.add(ball);

玻璃材质

能跟我们环境相关的材质处理金属以外,最常见的就还有玻璃。 我们使用MeshPhysicalMaterial来实现玻璃材质的效果。

对比金属材质,玻璃的效果需要多设置以下的两个属性

transmission 透光率

ior:折射率,不同的材质的折射率有所不同

折射率表:

物质名称

分子式或符号

密度(g/cm³)

温度(℃)

折射率

丙酮

CH3COCH3

0.791

20

1.3593

甲醇

CH3OH

0.794

20

1.3290

乙醇

C2H5OH

0.800

20

1.3618

C6H6

0.879 [1] 

20

1.5012

二硫化碳

CS2

1.263

20

1.6276

四氯化碳

CCl4

1.591

20

1.4607

三氯甲烷

CHCl3

1.489

20

1.4467

乙醚

C2H5OC2H5

0.715

20

1.3538

甘油

C3H8O3

1.260

20

1.4730

松节油

0.87

20.7

1.4721

橄榄油

0.92

0

1.4763

H2O

1.00

20

1.3330

熔凝石英

SiO2

1.45843

氯化钠

NaCl

1.54427

氯化钾

KCl

1.49044

萤石

CaF2

1.43381

冕牌玻璃

K6

1.51110

K8

1.51590

K9

1.51630

重冕玻璃

ZK6

1.61263

ZK8

1.61400

钡冕玻璃

BaK2

1.53988

火石玻璃

F1

1.60328

钡火石玻璃

BaF8

1.62590

重火石玻璃

ZF1

1.64752

ZF5

1.73977

ZF6

1.75496

const glassMaterial = new THREE.MeshPhysicalMaterial({
    transmission: 1, // 设置材质的透光率
    ior: 1.5, // 材质的折射率 0 - 2.33
    metalness: 0,
    roughness: 0.1,
    envMap: cubeTexture,
    envMapIntensity: 1 // 环境贴图对于Mesh表面的影响程度
})
const ball2 = new THREE.Mesh(geometry, glassMaterial);
ball2.position.x = 3
scene.add(ball2);
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CUBE</title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }

    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        // 通过被scene的background属性 可以i设置为一个单独的颜色
        // scene.background = new THREE.Color('skyblue')
        const cubeTexture = new THREE.CubeTextureLoader()
            .setPath('../../three.js/examples/textures/cube/Park2/') // setPath可以设置贴图的文件放置目录
            .load([  // 加载6个面对应的贴图材料  顺序不能乱改
                'posx.jpg',
                'negx.jpg',
                'posy.jpg',
                'negy.jpg',
                'posz.jpg',
                'negz.jpg',
            ])
        scene.background = cubeTexture;
        const geometry = new THREE.SphereGeometry(1, 32, 16);
        // const material = new THREE.MeshBasicMaterial( { color: 0xff6600 } );  // 基础材料 不受光线的影响
        const material = new THREE.MeshStandardMaterial({
            metalness: 1.0, // 金属度
            roughness: 0, // 粗糙程度 1 表示漫反射 0完全光滑(镜面效果更明显)
            envMap: cubeTexture,  //给材料也设置一个指定的环境贴图
            envMapIntensity: 1
        })
        const ball = new THREE.Mesh(geometry, material);
        scene.add(ball);
        // MeshPhysicalMaterial  使用物理材料可以对材质设置 光线的折射率透光率等属性
        const glassMaterial = new THREE.MeshPhysicalMaterial({
            transmission: 1, // 设置材质的透光率
            ior: 1.1, // 材质的折射率 0 - 2.33
            metalness: 0.2,
            roughness: 0.2,
            envMap: cubeTexture,
            envMapIntensity: 1 // 环境贴图对于Mesh表面的影响程度
        })
        const ball2 = new THREE.Mesh(geometry, glassMaterial);
        ball2.position.x = 3
        scene.add(ball2);
        // 添加环境光
        scene.add(new THREE.AmbientLight(0xffffff, 1));
        // 添加直线光源
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
        directionalLight.position.set(50, 50, 50)
        scene.add(directionalLight)
        // 创建了一个透视相机实例
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
        camera.position.z = 5;
        // 相机控制
        const controls = new OrbitControls(camera, renderer.domElement);
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

光和阴影

在学了环境贴图和几何体之后我们对于3d世界当时的效果要求就会越来越高,所以我们对于有光线的场景当中的阴影也会有一定的要求,这样做出的3d场景会更加的真实。

具体的实现流程

给渲染器开启阴影配置

// 开启渲染器的阴影渲染
renderer.shadowMap.enabled = true;

新建一个地板模型

我们会让光线照射几何体然后在地板上呈现出光影的效果

 // 添加一个地板
const floorGeogemtry =  new THREE.PlaneGeometry(30,30);
const material = new THREE.MeshLambertMaterial({
   color: 0xffffff
})
const floor = new THREE.Mesh(floorGeogemtry, material )
floor.rotateX(-Math.PI/2); // 平放

阴影最终是落在地板上,所以地板模型需要开启接收阴影生成的配置

floor.receiveShadow = true;

创建一个几何体

几何的位置要在平板之上

const ballGeomerty = new THREE.SphereGeometry(1,30,16);
const ball =  new THREE.Mesh( ballGeomerty , material);
ball.position.y  = 2;

我们的阴影是通过几何体生成的所以需要设置阴影属性配置

ball.castShadow  = true;

开始添加光源

最常见的能形成阴影的三种光源是:

DirectionalLight 平行光 之沿着一个方向照射不会发散的光源 ( 一般用来模拟太阳直射 )

PointLight 点光源,由一个点开始沿着这个点的各个方向发散的光 ( 灯泡发出的光 )

SpotLightHelper 聚光源, 有一个点沿着一个方向带有一定发散角度的光 ( 舞台的聚光灯)

// 平行光源一般是用来模拟大自然当中的 阳光直射的效果
const directLight = new THREE.DirectionalLight(0xffffff,0.5);
directLight.position.set(50,50,50); // 光源的位置
// 对于光线我们也要开启 castShadow  才能产生阴影效果
directLight.castShadow   = true; 
scene.add( directLight)

光源对象也要开启阴影属性

directLight.castShadow = true;

通过GUI去调试不同的位置的光源的效果

//  记得要在顶部想引入 GUI  import { GUI } from  'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI();
gui.add(lightPos, 'x', 30, 80).name('光源的x').onChange(val => pointLight.position.x = val)
gui.add(lightPos, 'y', 30, 80).name('光源的y').onChange(val => pointLight.position.y = val)
gui.add(lightPos, 'z', 30, 80).name('光源的z').onChange(val => pointLight.position.z = val)

配合环境光

如果我们需要让场景更加的接近现实场景,我们一般还会配合环境光的使用

// 一般而言 3d场景还可以配合环境光 呈现更立体的效果
const ambientlight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientlight)
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width= , initial-scale=1.0">
    <title></title>
</head>

<body>
    <!-- importmap 其实允许我们对于文件的导入路径进行一个映射  -->
    <script type="importmap">
        {
            "imports": {
                "three":"../../three.js/build/three.module.js",
                "three/addons/":"../../three.js/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
        const scene = new THREE.Scene();
        const renderer = new THREE.WebGLRenderer({
            // alpha: true, // 背景是否可以设置透明色
            antialias: true, //表示是否开启反锯齿
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        // 开启渲染器的阴影渲染
        renderer.shadowMap.enabled = true;
        // 通过渲染器 可以设置阴影的贴图算法
        // THREE.BasicShadowMap 性能很好,但质量很差
        // THREE.PCFShadowMap 性能较差,但边缘更光滑
        // THREE.PCFSoftShadowMap 性能较差,但边缘更柔软
        renderer.shadowMap.type = THREE.PCFSoftShadowMap
        // 添加一个地板
        const floorGeogemtry = new THREE.PlaneGeometry(30, 30);
        const material = new THREE.MeshLambertMaterial({
            color: 0xffffff
        })
        const floor = new THREE.Mesh(floorGeogemtry, material)
        floor.rotateX(-Math.PI / 2)
        // 对于要接收阴影的几何体需要开启  接受阴影的配置
        floor.receiveShadow = true;
        scene.add(floor);
        // 创建一个几何体
        const ballGeomerty = new THREE.SphereGeometry(1, 30, 16);
        const ball = new THREE.Mesh(ballGeomerty, material);
        ball.position.y = 2;
        // 对于产生阴影的几何体我们需要设置一个 castShadow  
        ball.castShadow = true;
        scene.add(ball)
        // 添加光源  平行光  点光源  聚光源 这三个光源都会产生 阴影的效果
        // 平行光源一般是用来模拟大自然当中的 阳光直射的效果
        const directLight = new THREE.DirectionalLight(0xffffff, 0.5);
        directLight.position.set(50, 50, 50); // 光源的位置
        // 对于光线我们也要开启 castShadow  才能产生阴影效果
        directLight.castShadow = true;
        scene.add(directLight)
        // // 添加平行光光线的辅助器
        // scene.add(new THREE.DirectionalLightHelper(directLight, 5))
        //  点光源
        // const pointLight = new THREE.PointLight(0xffffff, 1);
        // pointLight.position.set(50, 50, 50);
        // scene.add(pointLight);
        // pointLight.castShadow = true
        // // 添加点光源的辅助器
        // scene.add( new THREE.PointLightHelper(pointLight,5))
        // // 默认的阴影效果有时候会比较差 我们可以通过修改阴影贴图的大小来让它的效果进行优化
        // // 通过修改阴影贴图的大小来优化 ( 默认是512*512)
        // pointLight.shadow.mapSize.width = 1024
        // pointLight.shadow.mapSize.height = 1024
        // // 可以给阴影设置一个边缘模糊
        // pointLight.shadow.radius = 3
        // 聚合光
        const spotLight = new THREE.SpotLight(0xffffff);
        spotLight.position.set(15, 15, 15);
        spotLight.castShadow = true;
        scene.add(spotLight)
        scene.add(new THREE.SpotLightHelper(spotLight))
        // 一般而言 3d场景还可以配合环境光 呈现更立体的效果
        const ambientlight = new THREE.AmbientLight(0xffffff, 0.2);
        scene.add(ambientlight)
        const lightPos = {
            x: 50,
            y: 50,
            z: 50,
        }
        // 通过GUI去观察不同的光源位置产生的阴影效果
        const gui = new GUI();
        gui.add(lightPos, 'x', 30, 80).name('光源的x').onChange(val => pointLight.position.x = val)
        gui.add(lightPos, 'y', 30, 80).name('光源的y').onChange(val => pointLight.position.y = val)
        gui.add(lightPos, 'z', 30, 80).name('光源的z').onChange(val => pointLight.position.z = val)
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 5;
        camera.position.x = 2
        // camera.position.y = 5;
        // 新建一个坐标系辅助线(辅助器)
        const axesHelper = new THREE.AxesHelper(10);
        scene.add(axesHelper);// 在整个场景当中添加坐标辅助线
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', () => {
            renderer.render(scene, camera)
        })
        renderer.render(scene, camera)
        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera)
        }
        animate();
    </script>
</body>

</html>

猜你喜欢

转载自blog.csdn.net/m0_65121454/article/details/132778403