【threejs】基础知识和一些动效

了解threejs

开门上threejs官网

  • threejs和webgl区别

首先两者都是用于创建 3D 图形和交互性的技术,但在使用,生态和性能上有所区别:
(1)使用上,webgl更底层更抽象,它可直接访问图形硬件,编写底层图形渲染代码(如,着色器,矩阵变换,顶点缓冲区,光源等);threejs更易使用,它建于webgl之上,专注创建3d内容而不是处理底层的图形编程。
(2)生态上,webgl可查阅的文档资源较少;threejs有官网案例,开源项目还有辅助工具(如GUI,Tween,相机控件)生态更庞大些。
(3)性能上,如果对性能有严格要求,可直接使用webgl更精细的控制图片渲染流程,需要投入更多专业知识和时间;threejs对一些复杂场景做了一些优化和抽象,通常能提供足够的性能。
总之,webgl 更适合那些对图形编程有深入了解和对性能有更高要求的开发者,threejs易学易使用,专注3d内容但没有深入了解图形编程的人,根据需求和技术各取所需。

  • 基本使用流程

创建场景scene → 创建一个相机camera,设置相机位置 → 创建一个渲染器canvas,尺寸 → 创建几何体geometry,添加材质material → 添加灯光light → 把要展示的添加到场景里并渲染

复习3d三要素:视点(相机/眼睛),目标(几何体),上方向

官网基本场景案例
以下是常用光源、材质、几何体属性合集

const itemType = {
    SpotLight: ['color', 'intensity', 'distance', 'angle', 'exponent'], // 聚光灯
    AmbientLight: ['color'], // 环境光
    PointLight: ['color', 'intensity', 'distance'], // 点光源
    DirectionalLight: ['color', 'intensity'], // 平行光
    HemisphereLight: ['skyColor', 'groundColor', 'intensity'], // 半球光
    MeshBasicMaterial: ['color', 'opacity', 'transparent', 'wireframe', 'visible'], // 基础,显示简单颜色。
    MeshDepthMaterial: ['wireframe', 'cameraNear', 'cameraFar'], // 深度,指与相机距离越远越暗
    MeshNormalMaterial: ['opacity', 'transparent', 'wireframe', 'visible', 'side'], // 法向量,把法向量映射到RGB颜色的材质
    MeshLambertMaterial: ['opacity', 'transparent', 'wireframe', 'visible', 'side', 'ambient', 'emissive', 'color'], // 郎伯,良好暗淡效果,没有镜面高光
    MeshPhongMaterial: ['opacity', 'transparent', 'wireframe', 'visible', 'side', 'ambient', 'emissive', 'color', 'specular', 'shininess'], //phong,有镜面高光
    ShaderMaterial: ['red', 'alpha'], // 着色器,可自定义应用所有光照场景
    LineBasicMaterial: ['color'], // 实线
    LineDashedMaterial: ['dashSize', 'gapSize'], // 虚线
    PlaneGeometry: ['width', 'height', 'widthSegments', 'heightSegments'], // 平面
    PlaneBufferGeometry: ['width', 'height', 'widthSegments', 'heightSegments'], // 缓冲,顶点数据索引缓存,有效减少向 GPU 传输
    CircleGeometry: ['radius', 'segments', 'thetaStart', 'thetaLength'], // 圆
    BoxGeometry: ['width', 'height', 'depth', 'widthSegments', 'heightSegments', 'depthSegments'], // 矩形
    SphereGeometry: ['radius', 'widthSegments', 'heightSegments', 'phiStart', 'phiLength', 'thetaStart', 'thetaLength'], // 球
    CylinderGeometry: ['radiusTop', 'radiusBottom', 'height', 'radialSegments', 'heightSegments', 'openEnded'], // 圆柱
    TorusGeometry: ['radius', 'tube', 'radialSegments', 'tubularSegments', 'arc'], // 圆环
    TorusKnotGeometry: ['radius', 'tube', 'radialSegments', 'tubularSegments', 'p', 'q', 'heightScale'], // 扭结
    PolyhedronGeometry: ['radius', 'detail'], // 多面体
    TetrahedronGeometry: ['radius', 'detail'], // 四面体
    OctahedronGeometry: ['radius', 'detail'], // 八面体
    IcosahedronGeometry: ['radius', 'detail'], //二十面体
    TextGeometry: ['size', 'bevelThickness', 'bevelSize', 'bevelEnabled', 'bevelSegments', 'curveSegments', 'steps'], // 文字

设置阴影

设置对光有反应的材质 → 开启几何体阴影 → 使用平面接收 → 开启灯光阴影

(1)不是所有材质都对光有反应
(2)不是所有光都产生阴影

(1)对光有反应的材质:郎伯材质MeshLambertMaterial,MeshPhongMaterial,MeshStandardMaterial,MeshPhysicalMaterial
(2)可产生明确阴影的光:点光源SpotLight,平行光Directionallight

		// 几何开启阴影
        cube.castShadow = true;
        // 使用平面接收阴影
        plane.receiveShadow = true;
        // 设置灯光开启阴影
        spotLight.castShadow = true;
        renderer.shadowMapEnabled = true;
        spotLight.shadowMapWidth = 4096;//使阴影更清晰

加载外部文件

需要添加相关文件加载器xxxLoader

  const loader = new THREE.OBJMTLLoader()
  loader.load('../assets/models/city.obj', '../assets/models/city.mtl', (mesh) => {
    scene.add(mesh);
  });
  import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';

const fbxLoader = new THREE.FBXLoader();
fbxLoader.load('urlxxx.fbx', (object) => {
	// 这里object是group对象
	// 遍历场景中的所有几何体数据
    object.traverse((child) => {
    	if (child.isMesh) {
	    	// 对模型数据进行后期二次处理
	    	// scene.add(child);
        }
    });
}

补充:组对象Group、层级对象
(1)创建var group = new THREE.Group();
(2)添加group.add(mesh1);
(3)查看子对象group.children
(4)删除group.remove(mesh1);
(5)遍历group.traverse((child)=>{});

使用GUI绘制控制面板

辅助工具,在动画中为属性赋值对应的变量

//需要控制的属性
const controls = {
    color: '', // 是否要组合成立方体
    width: '',
};
let geo;
const gui = new dat.GUI();
for (const key in controls) {
	if(key=='color'){
	    gui.addColor(controls, 'color',key).onChange((value) => {
	    	controls.color = value;
        });
	}else{
	    gui.add(controls, key).onChange(() => {
     		// 更新几何体属性,width等尺寸需要如下操作
     		//1. 先删除
     		scene.remove(geo);
     		//2. 再添加
        	const geo = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10, 10, 10, 10), new THREE.MeshNormalMaterial())
        	scene.add(geo);
      	});
	}
}

Tweenjs动画

中文文档

优点:

  1. 支持多种动画类型:Tween.js 可以创建各种类型的动画效果,包括数字变化、颜色渐变、位置移动、缩放变换等。你可以对不同的属性进行动画化,实现更丰富多样的效果。Tween.js 还提供了丰富的缓动函数(easing functions),可帮助你实现自定义的动画变化曲线。
  2. 时间控制和事件回调:Tween.js 允许你控制动画的开始、暂停、恢复和停止。你可以根据需要随时进行时间控制,以适应交互或其他场景的需求。此外,Tween.js 还支持在动画达到特定时间点或完成时触发回调函数,以便执行进一步的操作或处理事件。
        new TWEEN.Tween(cube.rotation).to({
            x: cube.rotation.x + 2,
            y: cube.rotation.y + 2,
            z: cube.rotation.z + 2,
        }, 2000).start().repeat(Infinity);
        
        //动画渲染/循环
        function animate() {
            // cube.rotation.x += 0.01;
            // cube.rotation.y += 0.01;
            TWEEN.update();
            // 渲染
            renderer.render(scene, camera);
            requestAnimationFrame(animate);//浏览器下次重绘之前执行回调
        }
        animate();

补充:页面中多次使用TWEEN.update()的坑

相机控件

  1. 这里就说常用的Orbitcontrols(轨道控制器),可以使得相机围绕目标进行轨道运动,可以通过鼠标多方面拖拽观察模型。
    // 相机(透视)
    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 100000);
    camera.position.set(1000, 500, 100);
    scene.add(camera);
    // 添加相机控件-轨迹
    const controls = new OrbitControls(camera, canvas);
    // 是否有惯性
    controls.enableDamping = true;
    // 是否可以缩放
    // controls.enableZoom = true;
    controls.enableZoom = false; // 采用鼠标滚轮

注意: 采用鼠标滚轮,一定把相机控件的缩放关闭controls.enableZoom = false;

  1. 让场景根据鼠标位置进行缩放

思路主要以下:鼠标桌标计算、坐标转换unproject、统一化normalize、改变相机中心点

    addWheel() {
        const body = document.body;
        body.onmoussewheel = (event) => {
            const value = 30;

            //获取鼠标坐标位置
            const x = (event.clientX / window.innerWidth) * 2 - 1;
            const y = -(event.clientY / window.innerHeight) * 2 + 1;

            // 获取屏幕坐标
            const vector = new THREE.Vector3(x, y, 0.5);
            // 将屏幕坐标转换为three.js场景坐标(鼠标点击位坐标置转三维坐标)
            vector.unproject(this.camera);
            // 获取缩放的坐标信息
            vector.sub(this.camera.position).normalize();

            if (event.wheelDelta > 0) {
                //针对相机做处理
                this.camera.position.x += vector.x * value;
                this.camera.position.y += vector.y * value;
                this.camera.position.z += vector.z * value;
                controls.target.x += vector.x * value;
                controls.target.y += vector.y * value;
                controls.target.z += vector.z * value;
            } else {
                this.camera.position.x -= vector.x * value;
                this.camera.position.y -= vector.y * value;
                this.camera.position.z -= vector.z * value;
                controls.target.x -= vector.x * value;
                controls.target.y -= vector.y * value;
                controls.target.z -= vector.z * value;
            }
        }
    }

一些效果

雾化-fog

scene.fog = new THREE.Fog(0xffffff, 1, 50);

辉光-后期处理通道 pass

RenderPass二次处理 → OutlinePass配置辉光属性 → EffectComposer组合器组合以上通道 → 渲染组合render

        //辉光效果
        // 创建了一个渲染通道,这个通道会渲染场景,不会渲染到屏幕上
        const renderScene = new RenderPass(scene, camera);//对图像做二次处理
        // 分辨率 场景 相机 当前选中的物体(需要添加辉光效果)
        const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera, [cube1, cube2])
        outlinePass.renderToScreen = true; // 渲染到屏幕上
        outlinePass.edgeStrength = 3; // 尺寸
        outlinePass.edgeGlow = 2; // 发光的强度
        outlinePass.edgeThickness = 2; // 光晕粗细
        outlinePass.pulsePeriod = 1;// 闪烁的速度,值越小闪烁越快
        outlinePass.visibleEdgeColor.set('yellow');

        // 创建一个组合器对象,添加处理通道
        const bloom = new EffectComposer(renderer)
        bloom.setSize(window.innerWidth, window.innerHeight)
        bloom.addPass(renderScene)
        bloom.addPass(outlinePass)
        
        //动画渲染/循环
        function animate() {
            renderer.render(scene, camera);
            bloom.render();
            requestAnimationFrame(animate);//浏览器下次重绘之前执行回调
        }
        animate();

反光-环境贴图

创建虚拟场景盒子skybox → 设置几何体cube材质 → cubeCamera获取cube材质renderTarget → 给反光几何添加上反光材质envMap为renderTarget

		// 添加轨道控件
 		const controls = new THREE.OrbitControls(camera)
        // 环境纹理,虚拟反光效果
        // 创建虚拟的场景
        const imgs = [
            './assets/img/sky/right.jpg',
            './assets/img/sky/left.jpg',
            './assets/img/sky/top.jpg',
            './assets/img/sky/bottom.jpg',
            './assets/img/sky/front.jpg',
            './assets/img/sky/back.jpg',
        ]

        const mats = [];
        for (let i = 0; i < imgs.length; i++) {
            mats.push(new THREE.MeshBasicMaterial({
            	//bumpmap凹凸,normalmap法向
                map: THREE.ImageUtils.loadTexture(imgs[i]),
                side: THREE.DoubleSide,// 环境贴图需要设置,默认frontside前外面,backside后内面,doubleside两面
            }))
        }
        // 虚拟环境盒子
        const skybox = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), new THREE.MeshFaceMaterial(mats))
        scene.add(skybox)

        // 创建一个球体 和一个立方体
        const sphereGeometry = new THREE.SphereGeometry(4, 15, 15);
        const cubeGeometry = new THREE.BoxGeometry(5, 5, 5);

        // 立方体贴图是和环境一致, 球体是跟随当前环境
        const cubeMaterial = new THREE.MeshBasicMaterial({
            envMap: THREE.ImageUtils.loadTextureCube(imgs)//使用和天空盒子一样材质
        })

        // 通过立方体相机来实现
        const cubeCamera = new THREE.CubeCamera(0.1, 2000, 256);
        scene.add(cubeCamera);

        const sphereMaterial = new THREE.MeshBasicMaterial({
            envMap: cubeCamera.renderTarget,
        })

        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
        const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

        sphere.position.x = 5;
        cube.position.x = -5;
        scene.add(sphere)
        scene.add(cube)
        
        const clock = new THREE.Clock();
        //动画渲染/循环
        function animate() {
            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;
            controls.update(clock.getDelta());//动画添加相机更新
            // 渲染
            renderer.render(scene, camera);
            requestAnimationFrame(animate);//浏览器下次重绘之前执行回调
            cubeCamera.updateCubeMap(renderer, scene);//反光
        }
        animate();

渐变色-ShaderMaterial

  1. 主要使用到一个mix函数,该函数用于对两个值进行线性插值。它的作用是根据一个插值因子(介于0和1之间的值,),在两个输入值之间进行混合。语法:mix(value1, value2, factor)

value1:第一个输入值。
value2:第二个输入值。
factor:插值因子,控制两个输入值之间的混合比例。取值范围为0到1,其中0表示完全使用 value1,1表示完全使用 value2。

  1. 自定义着色器ShaderMaterial
名称描述
vertexShader定义顶点着色器(gl_Position,gl_PointSize)
fragmentShader定义片元着色器(gl_FragColor,gl_FragCoord)
uniforms所有顶点都具有相同的值的变量
attributes只在顶点着色器中,只能声明全局变量
varying从顶点着色器向片元着色器传递数据
transparenttrue,使得着色器支持透明
depthTestTHREE.DoubleSide,解决建筑物展示部分问题
sidetrue,可被建筑物遮挡隐藏
- - -- - -
    const material = new THREE.ShaderMaterial({
      uniforms: {
        u_city_color: {
          // 得需要一个模型颜色 最底部显示的颜色
          value: new THREE.Color('#1B3045')
        },
        u_head_color: {
          // 要有一个头部颜色 最顶部显示的颜色
          value: new THREE.Color('#ffffff')
        },
        u_size: {
          value: 100,
        },
      },
      vertexShader: `
        varying vec3 v_position;
        
        void main() {
          v_position = position;

          gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(v_position, 1.0);
        }
      `,
      fragmentShader: `
        varying vec3 v_position;

        uniform vec3 u_city_color;
        uniform vec3 u_head_color;
        uniform float u_size;
        
        void main() {
          vec3 base_color = u_city_color;
          base_color = mix(base_color, u_head_color, v_position.z / u_size);

          gl_FragColor = vec4(base_color, 1.0);
        }
      `,
    })

补充实现渐变效果有哪些:
(1)css3线性渐变样式
div {background: linear-gradient(45deg, #ff0000, #00ff00);}
(2)渐变图片做贴图

飞线-贝塞尔曲线

  1. 效果描述:从起点飞一个弧度到终点
  2. 准备:起点source、终点target、中点center,弧度高度height,飞线长度range、粒子大小size
  3. 中点获取:通过lerp函数,该函数用于在两个值之间进行线性插值。它的作用是根据一个插值因子,在两个输入值之间生成平滑过渡的值。语法:lerp(value1, value2, factor)(参数同mix函数)
        // 通过起始点和终止点来计算中心位置
        const center = target.clone().lerp(source, 0.5);
        // 设置中心位置的高度
        center.y += options.height;
  1. 绘制飞线:使用二次贝塞尔曲线,通过中心点控制飞线

贝塞尔曲线:是一种平滑曲线,由控制点定义。二次由1个控制点,三次由2个控制点,常用于平滑路径动画、形状变形。

  1. 获取粒子
 		// 起点到终点的距离,这里是粒子数量
 		const len = parseInt(source.distanceTo(target));
         // 获取粒子
        const points = curve.getPoints(len);
  1. 着色器定义位置Attribute(Float32BufferAttribute)
        const positions = [];//粒子坐标集合
        const aPositions = [];//粒子索引集合
        points.forEach((item, index) => {
            positions.push(item.x, item.y, item.z)
            aPositions.push(index)
        })
        const geometry = new THREE.BufferGeometry();//空几何图形

        geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))//第二参数表示获取数据数量
        geometry.setAttribute('a_position', new THREE.Float32BufferAttribute(aPositions, 1))

补充:Geometry和BufferGeometry使用区别
它们都是用于描述和存储3D对象的数据结构。
(1)Geometry,使用js对象来存储几何数据,适用简单模型,优点易创建,缺点就是js对象存储额外消耗内存和cpu。
(2)BufferGeometry,使用TypedArray更底层方式存储几何数据,适用于处理大量顶点数据的复杂场景,优点少内存,缺点相对难创建。
geometry.vertices可获取顶点数据

  1. 飞线长度:主要作用是控制size和opacity实现拖尾
       const material = new THREE.ShaderMaterial({
            uniforms: {
                u_color: {
                    value: new THREE.Color(options.color)
                },
                u_range: {
                    value: options.range
                },
                u_size: {
                    value: options.size
                },
                //粒子数量
                u_total: {
                    value: len,
                },
                u_time: this.time,
            },
            vertexShader: `
                attribute float a_position;
                
                uniform float u_time;
                uniform float u_size;
                uniform float u_range;
                uniform float u_total;
            
                varying float v_opacity;
                
                void main() {
                    float size = u_size;
                    float total_number = u_total * mod(u_time, 1.0);
                    
                    if (total_number > a_position && total_number < a_position + u_range) {
                    
                        // 拖尾效果,超出范围的大小为0
                        float index = (a_position + u_range - total_number) / u_range;
                        size *= index;
                        
                        
                        v_opacity = 1.0;
                    } else {
                        v_opacity = 0.0;
                    }
                    
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                    gl_PointSize = size / 10.0;
                }
            `,
            fragmentShader: `
                uniform vec3 u_color;
                varying float v_opacity;
                
                void main() {
                    gl_FragColor = vec4(u_color, v_opacity);
                }
            `,
            transparent: true, // 使得着色器支持透明度
        });

github完整代码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值