cdn引入 1 2 3 4 5 6 7 8 9 10 11 <script type ="importmap" > { "imports" :{ "three" :"./src/three.module.js" } } </script > <script type ="module" > import * as THREE from 'three' import {OrbitControls } from './src/OrbitControls.js' </script >
1 2 3 4 5 6 const controls = new OrbitControls (camera,render.domElement ) controls.addEventListener ('change' ,function ( ){ render.render (scene,camera) })
创建vue3项目 1 2 vite create object npm init vite@latest
安装threejs
1 import * as Three from 'three'
场景创建基本步骤
创建场景
创建相机
创建渲染器
添加到DOM中
添加物体
添加光源
创建场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <script setup lang ="ts" > import {ref,onMounted} from 'vue' import * as Three from 'three' const canvas = ref<any>()onMounted (()=> { const pos = { width :window .innerWidth , height :window .innerHeight } window .onresize = function ( ){ pos.width = window .innerWidth pos.height = window .innerHeight camera.aspect = pos.width / pos.height camera.updateProjectionMatrix () renderer.setSize (pos.width , pos.height ) renderer.setPixelRatio (window .devicePixelRatio ) renderer.outputEncoding = THREE .sRGBEncoding ; renderer.shadowMap .enabled = true ; } const scene = new Three .Scene ({ antialias :true , powerPreference :'high-performance' , alpha :true , stencil :true , preserveDrawingBuffer :true , }) const geometry = new Three .BoxGeometry (3 , 3 , 3 ) const material = new Three .MeshBasicMaterial ({color : 0x00ff00 ,opacity :0.5 ,transparent :true }) const mesh = new Three .Mesh (geometry, material) mesh.position .set (1.5 ,1.5 ,1.5 ) group.add (mesh) const camera = new Three .PerspectiveCamera (75 , pos.width / pos.height , 0.1 , 1000 ) camera.position .set (5 , 5 , 20 ) scene.add (camera) const renderer = new Three .WebGLRenderer ({ canvas :canvas.value , antialias :true , }) renderer.render (scene, camera) }) </script > <template > <canvas ref ="canvas" > </canvas > </template >
创建一个模型组 1 2 3 4 5 6 7 const group = new Three .Group () scene.add (group) group.add (new Three .Mesh ( new Three .BoxGeometry (3 , 3 , 3 ), new Three .MeshBasicMaterial ({ color : 0xff0000 }) ))
设置模型位置的几种方式 1 2 3 4 5 6 7 mesh.position .set (1 ,2 ,3 ) mesh.position .x = 1 mesh.position .y = 2 mesh.position .z = 3 mesh.position = {x :1 ,y :2 ,z :3 }
设置模型旋转 1 2 3 4 mesh.rotation .x = Math .PI / 4 mesh.rotation .y = Math .PI / 4 mesh.rotation .z = Math .PI / 4 mesh.rotation = {x :Math .PI / 4 ,y :Math .PI / 4 ,z :Math .PI / 4 }
设置模型移动 1 2 3 4 5 6 mesh.translateX (1 ) mesh.translateY (2 ) mesh.translateZ (3 ) mesh.translateX (1 ,2 ,3 ) mesh.translateX (1 ,2 ,3 ) mesh.translateX ({x :1 ,y :2 ,z :3 })
设置模型缩放 1 2 3 4 mesh.scale .x = 2 mesh.scale .y = 2 mesh.scale .z = 2 mesh.scale = {x :2 ,y :2 ,z :2 }
创建辅助坐标轴 1 2 const axesHelper = new Three .AxesHelper (5 ) scene.add (axesHelper)
创建辅助网格 1 2 3 const gridHelper = new Three .GridHelper (10 , 10 ,0xff0000 ,0xefefef ) scene.add (grid)
辅助边界 为创建的模型添加矩形边界
1 2 3 4 const sphere = new Three .SphereGeometry (1 ,32 ,32 );const object = new Three .Mesh ( sphere, new Three .MeshBasicMaterial () );const box = new Three .BoxHelper ( object, 0xffff00 ); scene.add ( object,box );
主动创建一个辅助边界 1 2 3 4 5 const box = new THREE .Box3 (); box.setFromCenterAndSize ( new THREE .Vector3 ( 1 , 1 , 1 ), new THREE .Vector3 ( 2 , 1 , 3 ) );const helper = new THREE .Box3Helper ( box, 0xffff00 ); scene.add ( helper );
判断两个点之间的距离 1 2 console .log (new Three .Vector3 (0 , 10 , 0 ).distanceTo (mesh.position ))console .log (mesh.position .distanceTo (new Three .Vector3 (0 , 10 , 0 )))
通过第三方插件设置动画
1 2 3 4 5 6 gsap.to (group.position , { duration :1 , x :Math .sin (time) * 1.5 , y :Math .sin (time) * 1.5 , z :Math .sin (time) * 1.5 })
让模型动起来 恒定帧率渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let quan = 0 setInterval (()=> { group.rotation .x += 0.01 group.rotation .y += 0.01 if (quan < 360 ){ quan += 1 }else { quan = 1 } camera.position .x = Math .sin (quan * Math .PI / 180 ) * 10 camera.position .y = 10 camera.position .z = Math .cos (quan * Math .PI / 180 ) * 10 camera.lookAt (group.position ) renderer.render (scene, camera) },30 )
通过window.requestAnimationFrame实现
1 2 3 4 5 6 7 8 9 10 11 12 let lastTime = Date .now ()const tick = ( )=>{ let time = Date .now () const deltaTime = time - lastTime lastTime = time console .log (deltaTime) camera.position .x += 0.001 * deltaTime camera.position .z += 0.001 * deltaTime renderer.render (scene, camera) window .requestAnimationFrame (tick) }tick ()
通过threejs clock实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const clock = new Three .Clock ()let quan = 0 const tick = ( )=>{ let time = clock.getElapsedTime () quan = time % 365 * 30 camera.position .x = Math .sin (quan * Math .PI / 180 ) * 10 camera.position .z = Math .cos (quan * Math .PI / 180 ) * 10 camera.position .y = 10 camera.lookAt (group.position ) renderer.render (scene, camera) window .requestAnimationFrame (tick) }tick ()
使用数据控制相机移动 1 2 3 4 5 6 7 8 9 10 canvas.value .addEventListener ('mousemove' ,(event:any )=> { let x = event.offsetX / (pos.width / 2 ) - 0.5 let y = event.offsetY / (pos.height / 2 ) - 0.5 camera.position .x = Math .sin (x * Math .PI * 2 ) * 10 camera.position .z = Math .cos (x * Math .PI * 2 ) * 10 camera.position .y = Math .sin (y * Math .PI ) * 10 camera.lookAt (group.position ) })
内置事件 1.鼠标拖动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' const controls = new OrbitControls (camera, canvas.value ) controls.minDistance = 2 controls.maxDistance = 10 controls.enableDamping = true controls.minPolarAngle = 0 ; controls.maxPolarAngle = Math .PI / 2 ; controls.minAzimuthAngle = -Infinity ; controls.maxAzimuthAngle = Infinity ; controls.addEventListener ('change' , () => { }) controls.addEventListener ('start' , () => { }) controls.addEventListener ('end' , () => { }) controls.update ()
使用BufferGeometry创建几何体 1 2 3 4 5 6 7 8 9 10 11 12 13 const positionAttr = new Float32Array ([ 1 ,1 ,1 , 2 ,1 ,1 , 1 ,2 ,1 , 1 ,1 ,1 ])const lineGeometry = new Three .BufferGeometry () lineGeometry.setAttribute ('position' ,new Three .BufferAttribute (positionAttr,3 )) scene.add (new Three .Line (lineGeometry,new Three .LineBasicMaterial ({color :0xff00ff })))
创建线段 Line
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const lineMaterial = new Three .LineBasicMaterial ( { color : 0xffffff , linewidth : 1 , linecap : 'round' , linejoin : 'round' } );const points = []; points.push ( new Three .Vector3 ( - 10 , 0 , 0 ) ); points.push ( new Three .Vector3 ( 0 , 10 , 0 ) ); points.push ( new Three .Vector3 ( 10 , 0 , 0 ) );const geometry = new Three .BufferGeometry ().setFromPoints ( points );const line = new Three .Line ( geometry, lineMaterial ); scene.add ( line );
创建闭合线段 LineLoop
1 const line = new Three .LineLoop ( geometry, lineMaterial );
设置环境颜色 需要改变渲染器的颜色
1 2 3 4 5 const renderer = new Three .WebGLRenderer ({ canvas :canvas.value }) renderer.setClearColor (0xdfdfdf , 1 );
添加云雾效果 1 2 3 const fog = new Three .Fog (0xdfdfdf , 1 ,10 ) scene.fog = fog
相机 透视相机 符合人眼的透视效果
1 2 3 4 5 6 7 const camera = new Three .PerspectiveCamera (75 , pos.width / pos.height , 0.1 , 100 ) camera.position .set (10 ,20 ,30 ) camera.lookAt (group.position )const cameraHelper = new Three .CameraHelper (camera) scene.add (camera,cameraHelper)
光源 环境光 AmbientLight 环境光不能单独存在,单独添加不会有效果 环境光会均匀的照亮场景中的所有物体。 环境光不能用来投射阴影,因为它没有方向。 某些材质(如基本材质 BasicMaterial
)可能不支持光照效果,或者对光照的响应不明显。尝试使用支持光照的材质,如 MeshStandardMaterial
或 MeshPhongMaterial
。
1 2 3 const ambientLight = new Three .AmbientLight (0xffffff , 1 ) scene.add (ambientLight)
平行光 DirectionalLight 平行光是沿着特定方向发射的光。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果。 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的。
1 2 3 4 5 6 7 8 const directionalLight = new Three .DirectionalLight (0xffffff , 1 ) directionalLight.position .set (-2 ,3 ,4 ) directionalLight.target .position .set (0 ,-3 ,0 ) directionalLight.target .updateMatrixWorld () scene.add (directionalLight)const helper = new Three .DirectionalLightHelper ( light, 5 ); scene.add ( helper );
半球光 HemisphereLight 光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。 半球光不能投射阴影
1 2 3 const light = new THREE .HemisphereLight ( 0xffffbb , 0x080820 , 1 ); const helper = new THREE .HemisphereLightHelper ( light, 5 ); scene.add ( light,helper );
点光源 PointLight 从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。
1 2 3 4 5 const pointLight = new Three .PointLight (0xffffff , 1 ) pointLight.position .set (-4 ,6 ,-2 ) const pointLightHelper = new THREE .PointLightHelper ( pointLight, 1 ); scene.add (pointLight,pointLightHelper)
平面光光源 RectAreaLight 平面光光源从一个矩形平面上均匀地发射光线。这种光源可以用来模拟像明亮的窗户或者条状灯光光源。 注意事项:
不支持阴影。
只支持 MeshStandardMaterial 和 MeshPhysicalMaterial 两种材质。
你必须在你的场景中加入 RectAreaLightUniformsLib,并调用 init()。
1 2 3 4 5 6 7 8 9 10 11 const width = 10 ;const height = 10 ;const intensity = 1 ; const rectLight = new THREE .RectAreaLight ( 0xffffff , intensity, width, height ); rectLight.position .set ( 5 , 5 , 0 ); rectLight.lookAt ( 0 , 0 , 0 ); scene.add ( rectLight )import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js' const rectLightHelper = new RectAreaLightHelper ( rectLight ); scene.add ( rectLightHelper );
聚光灯 SpotLight 光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大。 该光源可以投射阴影。
1 2 3 4 5 const spotLight = new Three .SpotLight (0xffffff , 1 ) spotLight.position .set (-4 ,6 ,-2 ) spotLight.map = new THREE .TextureLoader ().load ( url ); const spotLightHelper = new THREE .SpotLightHelper ( spotLight ); scene.add (spotLight,spotLightHelper)
添加测试组件 lil-gui官方文档
1 npm install lil-gui --save
使用
1 2 3 4 5 6 7 import GUI from 'lil-gui' const gui = new GUI () gui.add (camera,'position' ,'x' ,-10 ,10 ,0.1 ) gui.add (camera,'position' ,'y' ).min (-10 ).max (10 ).step (0.1 ).name ('y轴位置' )
1 2 gui.add (mesh,'visible' ) gui.add (mesh.material ,'wireframe' ).name ('材质' )
添加一个颜色选择器 直接修改会发现选择的颜色和材质的颜色不一致
1 gui.addColor (mesh.material ,'color' )
改良方法
1 2 3 4 5 6 7 8 9 let debugObject = { color :'#333333' }const material = new Three .MeshStandardMaterial ({color : debugObject.color ,opacity :1 ,transparent :true }) gui.addColor (debugObject,'color' ).name ('颜色' ).onChange ((value )=> { mesh.material .color = new Three .Color (value) mesh.material .color .set (value) })
添加按钮并添加事件 1 2 3 4 debugObject.move = function ( ){ mesh.position .x += 0.1 } gui.add (debugObject,'move' ).name ('移动' )
设置相机回到初始位置 1 2 3 4 5 6 7 8 9 debugObject.move = function ( ){ gsap.to (camera.position ,{ duration : 1 , x : Math .sin (0.5 ) * 10 , y : Math .sin (0.5 ) * 10 , z : Math .sin (0.8 ) * 10 }) } gui.add (debugObject,'move' ).name ('移动' )
修改网格数量 修改网格数量相对麻烦 没有直接修改的方法 所以只能先删除原来的 再添加新的
1 2 3 4 5 6 7 debugObject.geometry = { geometry :1 } gui.add (debugObject.geometry ,'geometry' ).name ('网格数量' ).min (1 ).max (10 ).step (1 ).onFinishChange ((e )=> { mesh.geometry .dispose () mesh.geometry = new Three .BoxGeometry (3 , 3 , 3 , e, e, e) })
创建一个编组 1 2 3 4 const folder = gui.addFolder ('编组1' ) folder.add (camera.position ,'x' ).min (1 ).max (100 ).step (0.01 ) folder.close ()
面板的基本参数设置 1 2 3 4 5 6 7 8 9 10 11 12 const gui = new GUI ({ width : 300 , title : '控制面板' , closeFolders : true , }) gui.close () gui.hide () gui.show () document .addEventListener ('keydown' , (e ) => { if (e.key === 'h' ) gui.show (gui._hidden ) })
帧数显示插件 stats.js github地址
1 npm install stats.js --save
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import Stats from 'stats.js' ;const clock = new Three .Clock ()var stats = new Stats (); stats.showPanel ( 0 ); document .body .appendChild ( stats.dom );function animate ( ) { let time = clock.getElapsedTime () stats.begin (); camera.position .x = Math .sin (time) * 10 camera.position .z = Math .cos (time) * 10 camera.lookAt (group.position ) controls.update () renderer.render (scene, camera) stats.end (); requestAnimationFrame ( animate ); }requestAnimationFrame ( animate );
材质 加载图片材质 麻烦的方式
1 2 3 4 5 6 7 8 9 10 const img = new Image () img.src = new URL ('@/assets/1.jpg' , import .meta .url ).href ; const texture = new Three .Texture (img) img.onload = function ( ) { console .log ('图片加载完成.' ) texture.needsUpdate = true ; }const material = new Three .MeshBasicMaterial ({map :texture})
使用threejs加载器
1 2 3 4 5 let material = '' const loader = new Three .TextureLoader () loader.load (new URL ('@/assets/1.jpg' , import .meta .url ).href , (texture ) => { material = new Three .MeshBasicMaterial ({map :texture}) })
配置信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 texture.repeat .x = 2 texture.repeat .y = 2 texture.wrapS = Three .RepeatWrapping texture.wrapT = Three .RepeatWrapping texture.anisotropy = 16 texture.offset .x = 0.5 texture.offset .y = 0.5 texture.rotation = Math .PI / 4 texture.center .x = 0.5 texture.center .y = 0.5 texture.generateMipmaps = false texture.minFilter = Three .NearestFilter texture.magFilter = Three .NearestFilter texture.colorSpace = Three .SRGBColorSpace
材料 基础材质 一种用于绘制线框样式几何体的材质。 不受光照影响的材质
1 2 3 4 5 6 7 8 9 const material = new Three .MeshBasicMaterial ({color :0xff0000 }) material.map = texture; material.color = new Three .Color (0xfffff ); material.wireframe = true ; material.transparent = true ; material.opacity = 0.5 ; material.side = Three .DoubleSide ; material.alphaMap = texture;
点材质 PointsMaterial 可以将物体的分段线段绘制为点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const loader = new Three .TextureLoader ()let texture = loader.load (new URL ('@/assets/bg.jpg' , import .meta .url ).href )const sphere = new Three .SphereGeometry (1 ,32 ,32 )const masterial = new Three .PointsMaterial ({ size :0.01 , sizeAttenuation :true map :texture, alphaMap :texture, alphaTest :0.5 , depthTest :true , depthWrite :false , blending :Three .AdditiveBlending , vertexColors :true , fog : Boolean , })const mesh = new Three .Points (sphere,masterial) scene.add (mesh)
创建固定个数的随机点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const masterial = new Three .PointsMaterial ({ color :0xff0000 , size :0.02 , sizeAttenuation :true })const bufferGeometry = new Three .BufferGeometry () const pointArr = new Float32Array (5000 * 3 )const colorArr = new Float32Array (5000 * 3 )for (let i = 0 ;i < pointArr.length ;i++){ pointArr[i] = Math .random () * 2 - 1 colorArr[i] = Math .random () } bufferGeometry.setAttribute ('position' , new Three .BufferAttribute (pointArr,3 )) bufferGeometry.setAttribute ('color' , new Three .BufferAttribute (colorArr,3 ))const m1 = new Three .Points (bufferGeometry,masterial) scene.add (m1)
让点动起来 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const clock = new Three .Clock ()var stats = new Stats (); stats.showPanel ( 0 ); document .body .appendChild ( stats.dom );function animate ( ) { let time = clock.getElapsedTime () stats.begin (); for (let i = 1 ;i < m1.geometry .attributes .position .array .length ;i+=3 ){ let x = m1.geometry .attributes .position .array [i - 1 ] m1.geometry .attributes .position .array [i] = Math .sin (time+x*5 ) } m1.geometry .attributes .position .needsUpdate = true controls.update () renderer.render (scene, camera) stats.end (); requestAnimationFrame ( animate ); }requestAnimationFrame ( animate );
法线网格材质 一种把法向量映射到RGB颜色的材质。
1 2 3 4 const material = new Three .MeshNormalMaterial () material.flatShading = true
材质捕捉 会在材质表面反射出加载图像的内容 像是照镜子 不受光照影响的材质
1 2 const material = new Three .MeshMatcapMaterial () material.matcap = new Three .TextureLoader ().load (new URL ('@/assets/1.jpg' ,import .meta .url ).href )
深度网格材质 根据距离摄像机的距离来显示材质 (距离越近显示越亮) 可以用来实现云雾等效果
1 const material = new Three .MeshDepthMaterial ()
Lambert网格材质 MeshLambertMaterial 一种非光泽表面的材质,没有镜面高光。 该材质使用基于非物理的Lambertian模型来计算反射率。 这可以很好地模拟一些表面(例如未经处理的木材或石材),但不能模拟具有镜面高光的光泽表面(例如涂漆木材)。 MeshLambertMaterial uses per-fragment shading。 由于反射率和光照模型的简单性,MeshPhongMaterial,MeshStandardMaterial或者MeshPhysicalMaterial 上使用这种材质时会以一些图形精度为代价,得到更高的性能。
1 const material = new Three .MeshLambertMaterial ()
Phong网格材质 MeshPhongMaterial 一种用于具有镜面高光的光泽表面的材质。 该材质使用非物理的Blinn-Phong模型来计算反射率。 与MeshLambertMaterial中使用的Lambertian模型不同,该材质可以模拟具有镜面高光的光泽表面(例如涂漆木材)。MeshPhongMaterial uses per-fragment shading。 在MeshStandardMaterial或MeshPhysicalMaterial上使用此材质时,性能通常会更高 ,但会牺牲一些图形精度。
1 const material = new Three .MeshPhongMaterial ()
标准网格材质 MeshStandardMaterial 一种基于物理的标准材质,使用Metallic-Roughness工作流程。 基于物理的渲染(PBR)最近已成为许多3D应用程序的标准,例如Unity, Unreal和 3D Studio Max。 这种方法与旧方法的不同之处在于,不使用近似值来表示光与表面的相互作用,而是使用物理上正确的模型。 我们的想法是,不是在特定照明下调整材质以使其看起来很好,而是可以创建一种材质,能够“正确”地应对所有光照场景。 在实践中,该材质提供了比MeshLambertMaterial 或MeshPhongMaterial 更精确和逼真的结果,代价是计算成本更高。MeshStandardMaterial uses per-fragment shading。
1 2 3 4 const material = new Three .MeshStandardMaterial ({ roughness : 0.5 , metalness : 0.2 , })
物理网格材质 MeshPhysicalMaterial MeshStandardMaterial的扩展,提供了更高级的基于物理的渲染属性: Clearcoat: 有些类似于车漆,碳纤,被水打湿的表面的材质需要在面上再增加一个透明的,具有一定反光特性的面。而且这个面说不定有一定的起伏与粗糙度。Clearcoat可以在不需要重新创建一个透明的面的情况下做到类似的效果。 基于物理的透明度:.opacity属性有一些限制:在透明度比较高的时候,反射也随之减少。使用基于物理的透光性.transmission属性可以让一些很薄的透明表面,例如玻璃,变得更真实一些。 高级光线反射: 为非金属材质提供了更多更灵活的光线反射。 Sheen: Can be used for representing cloth and fabric materials. 物理网格材质使用了更复杂的着色器功能,所以在每个像素的渲染都要比three.js中的其他材质更费性能,大部分的特性是默认关闭的,需要手动开启,每开启一项功能在开启的时候才会更耗性能。
1 const material = new Three .MeshPhysicalMaterial ()
卡通着色 MeshToonMaterial 一种实现卡通着色的材质。
1 const material = new Three .MeshToonMaterial ()
加载glb模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' ;const loader = new GLTFLoader ();const url = new URL ('@/assets/1.glb' , import .meta .url ).href ; loader.load ( url, (gltf ) => { console .log (gltf); gltf.scene .position .set (3 , 3 , 3 ); scene.add (gltf.scene ); }, undefined , (error ) => { console .error (error); } );
使用加载器管理加载状态 loadingManager LoadingManager只能监控加载的资源 不能监控自定义的资源 例如glb模型可以监控加载完成不能在回调中添加模型
1 2 3 4 5 6 7 8 9 10 11 12 const loadingManager = new Three .LoadingManager () loadingManager.onLoad = () => { console .log ('加载完成' ) } loadingManager.onProgress = (url,loaded,total ) => { console .log (`${url} 加载中 ${Math .floor(loaded / total * 100 )} %` ) } loadingManager.onError = (url ) => { console .log (`${url} 加载失败` ) }const url1 = new URL ('@/assets/1.jpg' ,import .meta .url )const texture = new Three .TextureLoader (loadingManager).load (url1)
使用google压缩模型文件(DRACOLoader) (draco github)[https://github.com/google/draco] (draco 官网)[https://google.github.io/draco/] 需要将node_modules\three\examples\jsm\libs\draco
文件夹放到public目录下
** 如果报错Uncaught SyntaxError: Unexpected token ‘<’ (at 7fde97c8-de31-412f-9f30-4f2af27bf265:2:1)** 则说明解码器文件路径不对 需要设置正确的路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' ;import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader' ;const dracoLoader = new DRACOLoader () dracoLoader.setDecoderPath ('public/draco/gltf/' ); dracoLoader.setDecoderConfig ({ type : 'js' });const loader = new GLTFLoader () loader.setDRACOLoader (dracoLoader) loader.load (new URL ('@/assets/glTF-Draco/Duck.gltf' ,import .meta .url ).href ,(model )=> { console .log (model) model.scene .scale .set (1 , 1 , 1 ) scene.add (model.scene ) })
使用模型的动画 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 let mixer = null ; loader.load ('/static/fox/Fox.gltf' ,(model )=> { mixer = new Three .AnimationMixer (model.scene ) const action = mixer.clipAction (model.animations [0 ]) action.play () scene.add (model.scene ) })const clock = new Three .Clock ()let endTime = 0 var stats = new Stats (); stats.showPanel ( 0 ); function animate ( ) { let time = clock.getElapsedTime () stats.begin (); if (mixer){ mixer.update (time - endTime) } renderer.render (scene, camera) endTime = time stats.end (); requestAnimationFrame ( animate ); }requestAnimationFrame ( animate );
设置阴影投影
设置渲染器
设置灯光
设置可投影的模型
设置接收阴影的模型
渲染器开启投影
1 2 3 renderer.shadowMap .enabled = true ; renderer.shadowMap .type = Three .PCFSoftShadowMap ;
光源开启投影
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const directionalLight = new Three .DirectionalLight (0xffffff , 1 ) directionalLight.position .set (10 ,13 ,20 ) directionalLight.castShadow = true directionalLight.shadow .camera .left = -10 directionalLight.shadow .camera .right = 10 directionalLight.shadow .camera .top = 10 directionalLight.shadow .camera .bottom = -10 directionalLight.shadow .camera .near = 0 directionalLight.shadow .camera .far = 100 directionalLight.shadow .mapSize .width = 2048 directionalLight.shadow .mapSize .height = 2048 directionalLight.shadow .radius = 20 directionalLight.shadow .bias = -0.001 directionalLight.shadow .normalBias = 0
需要投影的网格开启投影
1 2 3 4 const boxGeometry = new Three .BoxGeometry (1 , 1 , 1 )const material = new Three .MeshStandardMaterial ({color :0xff0000 })const mesh = new Three .Mesh (boxGeometry, material) mesh.castShadow = true
接收投影的网格开启接收
1 2 3 const plane2 = new Three .PlaneGeometry (10 , 10 )const mesh2 = new Three .Mesh (plane2, new Three .MeshStandardMaterial ({color :0xffffff })) mesh2.receiveShadow = true
静止的物体可以使用贴图来模拟投影 效果会更真实一些
1 2 3 4 5 6 7 const loader = new Three .TextureLoader ()let texture = loader.load (new URL ('@/assets/1.jpg' , import .meta .url ).href )const plane2 = new Three .PlaneGeometry (10 , 10 )const mesh = new Three .Mesh (plane2, new Three .MeshStandardMaterial ({map :texture})) mesh.rotation .x = - Math .PI / 2 mesh.receiveShadow = true scene.add (mesh)
使用alphaMap来模拟投影 1 2 3 4 5 6 7 8 9 10 const mesh = new Three .Mesh (plane2, new Three .MeshStandardMaterial ({ color :0xffff00 , transparent :true , opacity :0.5 , alphaMap :texture, })) mesh.rotation .x = - Math .PI / 2 mesh.receiveShadow = true scene.add (mesh)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const clock = new Three .Clock ()var stats = new Stats (); stats.showPanel ( 0 ); document .body .appendChild ( stats.dom );function animate ( ) { let time = clock.getElapsedTime () stats.begin (); box.position .y = Math .abs (1.5 *Math .sin (time))+0.5 mesh.scale .set (Math .abs (-0.5 *Math .sin (time)),Math .abs (-0.5 *Math .sin (time)),Math .abs (-0.5 *Math .sin (time))) mesh.material .opacity = 1 -Math .abs (Math .sin (time))+0.3 controls.update () renderer.render (scene, camera) stats.end (); requestAnimationFrame ( animate ); }requestAnimationFrame ( animate );
光线投射 Raycaster
1 2 const raycaster = new Three .Raycaster ()const rayOrigin = new Three .Vector3 (0 , 0 , 0 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const sphereGroup = new Three .Group ()for (let i = 0 ; i < 3 ; i++){ const sphere = createSphere (1 , 20 , 20 ,i * 3 , 0 , 0 , 0xff00ff ) sphereGroup.add (sphere) } scene.add (sphereGroup)const raycaster = new Three .Raycaster ()const rayOrigin = new Three .Vector3 (-2 , 0 , 0 )const rayDirection = new Three .Vector3 (10 , 5 , 0 ) rayDirection.normalize () raycaster.set (rayOrigin,rayDirection)const intersects = raycaster.intersectObjects (sphereGroup.children , true )const lineMaterial = new Three .LineBasicMaterial ( { color : 0xffffff , linewidth : 1 , linecap : 'round' , linejoin : 'round' });const points = []; points.push ( rayOrigin ); points.push ( rayDirection );const geometry = new Three .BufferGeometry ().setFromPoints ( points );const line = new Three .Line ( geometry, lineMaterial ); scene.add ( line );
计时器中更新射线 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const clock = new Three .Clock ()var stats = new Stats (); stats.showPanel ( 0 ); document .body .appendChild ( stats.dom );function animate ( ) { let time = clock.getElapsedTime () stats.begin (); for (const geometry of sphereGroup.children ){ geometry.material .color = new Three .Color (0xff00ff ) } const intersects = raycaster.intersectObjects (sphereGroup.children , true ) for (const interfsect of intersects){ interfsect.object .material .color = new Three .Color (0x0000ff ) } controls.update () renderer.render (scene, camera) stats.end (); requestAnimationFrame ( animate ); } requestAnimationFrame ( animate );
点击屏幕根据相机和鼠标位置获取相交的物体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const raycaster = new Three .Raycaster ()let mouse = new Three .Vector2 (); canvas.value .addEventListener ('click' , function (event ) { mouse.x = (event.offsetX / canvas.value .width ) * 2 - 1 ; mouse.y = -(event.offsetY / canvas.value .height ) * 2 + 1 ; raycaster.setFromCamera (mouse, camera); const intersect = raycaster.intersectObjects (sphereGroup.children ) for (const geometry of sphereGroup.children ){ geometry.material .color = new Three .Color (0xff00ff ) } for (const interfsect of intersect){ interfsect.object .material .color = new Three .Color (0x0000ff ) } const start = camera.position .clone (); const end = new Three .Vector3 ().addVectors (camera.position , raycaster.ray .direction .multiplyScalar (50 )); const geometry = new Three .BufferGeometry ().setFromPoints ([start, end]); const material = new Three .LineBasicMaterial ({ color : 0x00ff00 }); const rayLine = new Three .Line (geometry, material); scene.add (rayLine); });
添加文字模型 TextGeometry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js' import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js' const url = new URL ('@/assets/helvetiker_regular.typeface.json' ,import .meta .url )const loaderText = new FontLoader () loaderText.load (url.href , function (font ) { const geometry = new TextGeometry ('Hello three.js!' , { font : font, size : 1 , depth :.1 , curveSegments : 12 , bevelEnabled : true , bevelThickness : 0.03 , bevelSize : 0.03 , bevelSegments : 1 , }) geometry.computeBoundingBox () console .log (geometry.boundingBox ) geometry.center () const mesh = new Three .Mesh (geometry, new Three .MeshNormalMaterial ({ color : 0xff0000 })) mesh.castShadow = true scene.add (mesh) })
根据鼠标移动和滚轮来移动物体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const cameraGroup = new Three .Group ()const camera = new Three .PerspectiveCamera (35 , pos.width / pos.height , 0.1 , 1000 ) cameraGroup.add ( camera ); scene.add (cameraGroup) camera.position .z = 10 const mouse = { x :0 , y :0 }window .addEventListener ('mousemove' , function (event ){ mouse.x = (event.clientX / window .innerWidth ) - 0.5 mouse.y = -(event.clientY / window .innerHeight ) - 0.5 })function animate ( ) { let time = clock.getElapsedTime () stats.begin (); cameraGroup.position .y = - (window .scrollY -window .innerHeight /2 )/ 300 camera.position .x = mouse.x camera.position .y = mouse.y renderer.render (scene, camera) stats.end (); requestAnimationFrame ( animate ); } requestAnimationFrame ( animate );
添加缓动效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var timeEnd = 0 ; stats.showPanel ( 0 ); document .body .appendChild ( stats.dom );function animate ( ) { let time = clock.getElapsedTime () stats.begin (); cameraGroup.position .y = - (window .scrollY - window .innerHeight /2 ) / 300 camera.position .x += (mouse.x - camera.position .x ) * (time-timeEnd) * 5 camera.position .y += (mouse.y - camera.position .y ) * (time-timeEnd) * 5 renderer.render (scene, camera) stats.end (); timeEnd = time; requestAnimationFrame ( animate ); }requestAnimationFrame ( animate );
物理引擎(重力) 3D库
Ammo.js
Cannon.js
Oimo.js
2D库
Matter.js
P2.js
Planck.js
Box2D.js
cannon.js 安装
引用 1 import CANNON from 'cannon'
创建刚体世界 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const world = new CANNON .World () world.gravity .set (0 , -9.82 , 0 ) world.broadphase = new CANNON .SAPBroadphase (world) world.solver .iterations = 10 world.allowSleep = true const sphereBody = new CANNON .Body ({ mass : 1 }) const sphereShape = new CANNON .Sphere (0.5 ) sphereBody.addShape (sphereShape) sphereBody.position .set (0 , 1 , 0 ) world.addBody (sphereBody) const groundBody = new CANNON .Body ({ mass : 0 }) const groundShape = new CANNON .Plane () groundBody.addShape (groundShape) groundBody.position .set (0 , 0 , 0 ) groundBody.quaternion .setFromAxisAngle (new CANNON .Vec3 (1 , 0 , 0 ), -Math .PI / 2 ) world.addBody (groundBody) const clock = new Three .Clock ()var stats = new Stats (); stats.showPanel ( 0 ); document .body .appendChild ( stats.dom );function animate ( ) { let time = clock.getElapsedTime () stats.begin (); world.step (1 /60 ,time - endTime,3 ) sphereMesh.position .copy (sphereBody.position ) sphereMesh.quaternion .copy (sphereBody.quaternion ) renderer.render (scene, camera) stats.end (); requestAnimationFrame ( animate ); }requestAnimationFrame ( animate );
创建两种材质 并设置两种材质的物理属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const AMaterial = new CANNON .Material ('AMaterial' )const BMaterial = new CANNON .Material ('BMaterial' )const AContactMaterial = new CANNON .ContactMaterial (AMaterial , BMaterial , { friction : 0.3 , restitution : 0.5 , }) world.addContactMaterial (AContactMaterial ) sphereBody.material = BMaterial groundBody.material = AMaterial
默认材质 1 2 3 4 5 6 7 8 9 10 11 12 const defaultMaterial = new CANNON .Material ('default' )const defaultContactMaterial = new CANNON .ContactMaterial (defaultMaterial, defaultMaterial, { friction : 0.3 , restitution : 0.5 , }) world.addContactMaterial (defaultContactMaterial) world.defaultContactMaterial = defaultContactMaterial
添加力 1 2 3 4 5 6 7 8 9 sphereBody.applyForce (new CANNON .Vec3 (0 , 10 , 0 ), sphereBody.position ) sphereBody.applyImpulse (new CANNON .Vec3 (0 , 0 , 10 ), sphereBody.position ) sphereBody.applyLocalImpulse (new CANNON .Vec3 (0 , 0 , 10 ), sphereBody.position ) sphereBody.applyLocalTorque (new CANNON .Vec3 (0 , 0 , 1 ), sphereBody.position )
例子:施加一个力 1 2 sphereBody.applyLocalForce (new CANNON .Vec3 (10 , 0 , 0 ), sphereBody.position )
例子:施加一个持续的阻力 类似于风 写在animate函数中
1 sphereBody.applyForce (new CANNON .Vec3 (-0.5 , 0 , 0 ), sphereBody.position )
批量创建模型+刚体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 const defaultObj = { createShpere :function ( ){ console .log ('createShpere' ) createShpere (0.5 ,1 ,{x :Math .random ()*2 ,y :5 ,z :Math .random ()*2 },0x00fff0 ) }, createBox :function ( ){ console .log ('createBox' ) createBox ({x :0 ,y :5 ,z :0 }) }, } gui.add (defaultObj,'createShpere' ).name ('创建球体' ) gui.add (defaultObj,'createBox' ).name ('创建盒子' )const objArray = []const createShpere = function (radius, mass, position, color ){ const sphereGeometry = new Three .SphereGeometry (radius, 32 , 32 ) const sphereMaterial = new Three .MeshStandardMaterial ({ color :color, }) const sphereMesh = new Three .Mesh (sphereGeometry, sphereMaterial) sphereMesh.position .copy (position) sphereMesh.castShadow = true ; scene.add (sphereMesh) const sphereBody = new CANNON .Body ({ mass : mass, }) const sphere = new CANNON .Sphere (radius) sphereBody.addShape (sphere) sphereBody.position .copy (position) world.addBody (sphereBody) objArray.push ({ mesh :sphereMesh, body :sphereBody }) }createShpere (0.5 ,1 ,{x :0 ,y :5 ,z :0 },0x00fff0 )const createBox = function (position ){ let size = { x :Math .random ()*2 , y :Math .random ()*2 , z :Math .random ()*2 } const sphereGeometry = new Three .BoxGeometry (size.x , size.y , size.z ) const sphereMaterial = new Three .MeshStandardMaterial ({ color :0x00fff0 , }) const boxMesh = new Three .Mesh (sphereGeometry, sphereMaterial) boxMesh.position .copy (position) boxMesh.castShadow = true ; scene.add (boxMesh) const sphereBody = new CANNON .Body ({ mass : 1 , }) const sphere = new CANNON .Box (new CANNON .Vec3 (size.x /2 ,size.y /2 ,size.z /2 )) sphereBody.addShape (sphere) sphereBody.position .copy (position) world.addBody (sphereBody) objArray.push ({ mesh :boxMesh, body :sphereBody }) }createBox ({x :Math .random ()*5 ,y :8 ,z :Math .random ()*5 })for (let item of objArray){ item.mesh .position .copy (item.body .position ) item.mesh .quaternion .copy (item.body .quaternion ) }
监听刚体的碰撞事件 1 2 3 4 5 6 7 8 9 10 11 12 13 const audio = new Audio (new URL ('@/assets/h.mp3' ,import .meta .url ).href ) sphereBody.addEventListener ('collide' , function (e ){ console .log ('collide' ) const impactVelocity = e.contact .getImpactVelocityAlongNormal () if (impactVelocity > 1.5 ){ audio.volume = Math .random () audio.currentTime = 0 audio.play () } })
环境贴图 加载图片环境贴图 1 2 3 4 5 6 7 8 9 10 11 12 13 const imgLoader = new Three .CubeTextureLoader ();const bg = imgLoader.load ([ new URL ('@/assets/grass/color.jpg' ,import .meta .url ).href , new URL ('@/assets/grass/normal.jpg' ,import .meta .url ).href , new URL ('@/assets/grass/roughness.jpg' ,import .meta .url ).href , new URL ('@/assets/grass/ambientOcclusion.jpg' ,import .meta .url ).href , new URL ('@/assets/bricks/color.jpg' ,import .meta .url ).href , new URL ('@/assets/bricks/ambientOcclusion.jpg' ,import .meta .url ).href , ]) scene.environment = bg scene.background = bg scene.backgroundBlurriness = 0.1 scene.backgroundIntensity = 2
设置所有模型材质的强度 1 2 3 4 5 6 7 8 9 10 11 12 13 const updateAllMaterials = ( ) => { scene.traverse ((child ) => { if (child.isMesh && child.material .isMeshStandardMaterial ) { child.material .envMapIntensity = global .envMapIntensity } }) }
使用HDR文件添加贴图背景 更佳精细的效果 但是hdr文件会更大
1 2 3 4 5 6 7 8 9 import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' ;const rgbeLoader = new RGBELoader (); rgbeLoader.load (new URL ('@/assets/empty_play_room_4k.hdr' ,import .meta .url ).href , function (texture ) { texture.mapping = Three .EquirectangularReflectionMapping ; scene.background = texture; scene.environment = texture; scene.backgroundBlurriness = 0 scene.backgroundIntensity = 1 });
1 2 3 4 5 6 7 8 9 import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' ;const exrLoader = new EXRLoader (); exrLoader.load (new URL ('@/assets/empty_play_room_4k.exr' ,import .meta .url ).href , function (texture ) { texture.mapping = Three .EquirectangularReflectionMapping ; scene.background = texture; scene.environment = texture; scene.backgroundBlurriness = 0 scene.backgroundIntensity = 1 });
使用一张全景贴图来代替背景 1 2 3 4 5 6 7 const textureLoader = new Three .TextureLoader () textureLoader.load (new URL ('@/assets/grass/color.jpg' ,import .meta .url ).href , (texture ) => { texture.mapping = Three .EquirectangularReflectionMapping texture.colorSpace = Three .SRGBColorSpace scene.background = texture scene.environment = texture })
添加天空盒 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js' ;const textureLoader = new Three .TextureLoader () textureLoader.load (new URL ('@/assets/grass/color.jpg' ,import .meta .url ).href , (texture ) => { texture.mapping = Three .EquirectangularReflectionMapping texture.colorSpace = Three .SRGBColorSpace scene.environment = texture const groundedSkybox = new GroundedSkybox (texture,0.01 ,10 ) groundedSkybox.scale .setScalar (2 ) scene.add (groundedSkybox) })
天空模型 1 2 3 4 5 6 7 8 9 10 11 const groundGeo = new THREE .PlaneGeometry ( 10000 , 10000 );const groundMat = new THREE .MeshLambertMaterial ( { color : 0xffffff } ); groundMat.color .setHSL ( 0.095 , 1 , 0.75 );const ground = new THREE .Mesh ( groundGeo, groundMat ); ground.position .y = - 33 ; ground.rotation .x = - Math .PI / 2 ; ground.receiveShadow = true ; scene.add ( ground );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const vertexShader = ` varying vec3 vWorldPosition; void main() { vec4 worldPosition = modelMatrix * vec4( position, 1.0 ); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } ` ;const fragmentShader = ` uniform vec3 topColor; uniform vec3 bottomColor; uniform float offset; uniform float exponent; varying vec3 vWorldPosition; void main() { float h = normalize( vWorldPosition + offset ).y; gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( max( h , 0.0), exponent ), 0.0 ) ), 1.0 ); } ` ;const uniforms = { 'topColor' : { value : new THREE .Color ( 0x0077ff ) }, 'bottomColor' : { value : new THREE .Color ( 0xffffff ) }, 'offset' : { value : 33 }, 'exponent' : { value : 0.5 } }; uniforms[ 'topColor' ].value .copy ( hemiLight.color ); scene.fog .color .copy ( uniforms[ 'bottomColor' ].value );const skyGeo = new THREE .SphereGeometry ( 4000 , 32 , 15 );const skyMat = new THREE .ShaderMaterial ( { uniforms : uniforms, vertexShader : vertexShader, fragmentShader : fragmentShader, side : THREE .BackSide } );const sky = new THREE .Mesh ( skyGeo, skyMat ); scene.add ( sky );
物体反光和镜面效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const geometry = new Three .TorusGeometry ( 1.5 , 0.1 ); const material = new Three .MeshBasicMaterial ( { color : new Three .Color (10 ,10 ,10 ) } ); const torus = new Three .Mesh ( geometry, material ); torus.layers .enable (1 ); scene.add ( torus ); const box = new Three .BoxGeometry (1 , 1 , 1 )const boxMaterial = new Three .MeshStandardMaterial ({ color : 0xffffff , roughness : 0 , metalness : 1 , })const boxMesh = new Three .Mesh (box, boxMaterial) boxMesh.position .set (0 , 0.5 , 0 ) scene.add (boxMesh)const cubeRednerTarget = new Three .WebGLCubeRenderTarget ( 256 , { type : Three .HalfFloatType } ) scene.environment = cubeRednerTarget.texture const cubeCamera = new Three .CubeCamera (0.1 , 100 , cubeRednerTarget) cubeCamera.layers .set (1 ) cubeCamera.position .copy (boxMesh.position ) scene.add (cubeCamera)
渲染器设置曝光参数 1 2 3 4 5 6 7 8 9 10 11 12 renderer.toneMappingExposure = 0.5 gui.add (renderer, 'toneMapping' , { NoToneMapping : Three .NoToneMapping , LinearToneMapping : Three .LinearToneMapping , ReinhardToneMapping : Three .ReinhardToneMapping , CineonToneMapping : Three .CineonToneMapping , ACESFilmicToneMapping : Three .ACESFilmicToneMapping } )
GLSL的解释 GLSL(OpenGL Shading Language)是一种高级编程语言,专门设计用于编写运行在图形处理单元(GPU)上的程序。它是OpenGL的一部分,OpenGL是一个跨语言、跨平台的编程接口,用于渲染2D和3D矢量图形。GLSL允许开发者编写所谓的“着色器”(Shaders),这些着色器在GPU上执行,负责处理图形渲染管线中的不同阶段,如顶点着色(Vertex Shading)、片段着色(Fragment Shading)等。
GLSL的主要特点:
并行性:GLSL程序在GPU上并行执行,这意味着多个顶点或片段可以同时被处理,大大提高了图形渲染的效率。
硬件加速:由于GLSL程序运行在GPU上,它们可以利用GPU的并行处理能力,实现比CPU上运行的软件更快的图形处理。
与OpenGL紧密集成:GLSL是OpenGL的一部分,因此它可以直接访问OpenGL提供的各种资源和功能,如纹理、缓冲区对象等。
可编程性:通过GLSL,开发者可以自定义图形渲染管线中的各个阶段,实现自定义的渲染效果,如光照、阴影、纹理映射等。
跨平台:由于OpenGL是一个跨平台的API,GLSL程序也可以在支持OpenGL的多种硬件和操作系统上运行。
GLSL的基本结构: GLSL程序通常包括几个部分,每个部分对应图形渲染管线中的一个阶段:
顶点着色器(Vertex Shader):处理每个顶点的数据,如位置、颜色、纹理坐标等。它通常用于实现顶点变换、光照计算等。
片段着色器(Fragment Shader):处理每个片段(像素的候选者)的颜色和其他属性。它通常用于实现纹理映射、光照效果、颜色混合等。
几何着色器(Geometry Shader,可选):在顶点着色器之后运行,允许开发者创建或修改图元(如点、线、三角形)。
计算着色器(Compute Shader,较新的特性):用于执行通用计算任务,不直接参与图形渲染,但可以利用GPU的并行处理能力进行高性能计算。
GLSL程序通常使用类似于C的语法,但包含了一些专门用于图形处理的数据类型和函数。开发者需要熟悉OpenGL的渲染管线以及GPU的工作原理,才能有效地使用GLSL来创建复杂的图形效果。
着色器 需要使用到glsl
语言编写 在计算机图形学中,顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)是GPU编程中的两个核心组件,它们在图形渲染管道中扮演着至关重要的角色。以下是关于这两个着色器的详细解释:
顶点着色器(Vertex Shader) 顶点着色器是图形渲染管道的第一个可编程阶段。它的主要任务是对每个顶点进行处理,包括坐标变换、光照计算和其他顶点属性的计算。具体来说,顶点着色器执行以下操作:
坐标变换:将顶点坐标从模型空间转换到世界空间、视图空间和裁剪空间。这一过程中常用的变换矩阵包括模型矩阵、视图矩阵和投影矩阵。
光照计算:计算每个顶点的光照属性,如环境光、漫反射光和镜面反射光。
属性传递:将顶点属性(如颜色、纹理坐标、法线)传递到片段着色器,供后续处理使用。 顶点着色器处理后的顶点数据将作为后续渲染阶段(如光栅化、片段着色)的输入。
片段着色器(Fragment Shader) 片段着色器是图形渲染管道的最后一个可编程阶段。它的主要任务是计算每个片段(像素)的最终颜色值。具体来说,片段着色器执行以下操作:
颜色计算:根据顶点着色器传递过来的颜色和纹理采样,计算每个片段的颜色。
纹理采样:从纹理中采样颜色,并将其应用到片段上。
光照计算:进一步进行光照计算,以获得更加真实的光照效果。
特效实现:实现各种特效,如阴影、反射、折射等,以增强图像的视觉效果。 片段着色器处理后的片段数据将被合成到最终渲染的图像中。
两者之间的关系
数据传递:顶点着色器处理每个顶点的数据,并将结果(如变换后的顶点位置、颜色、纹理坐标等)传递给片段着色器。顶点着色器输出的插值变量会被片段着色器使用。
阶段顺序:顶点着色器在图形渲染管道的早期阶段运行,负责顶点的几何变换和属性计算;而片段着色器在后期阶段运行,负责计算顶点形成的图元(如三角形)内部每个片段的颜色。
并行执行:GPU通过并行执行大量顶点着色器和片段着色器来加速图形渲染。每个顶点和每个片段的处理都是独立的,这使得并行计算成为可能。
直接使用glsl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } ` ;const fragmentShader = ` uniform vec3 color; varying vec2 vUv; void main() { // 使用简单的颜色渐变效果,基于uv坐标 float intensity = mix(0.0, 1.0, vUv.y); gl_FragColor = vec4(color * intensity, 1.0); } ` ;const material = new Three .ShaderMaterial ({ vertexShader : vertexShader, fragmentShader : fragmentShader, uniforms : { color : { value : new Three .Color (0xff0000 ) } } });const geometry = new Three .BoxGeometry ();const cube = new Three .Mesh (geometry, material); scene.add (cube); renderer.render (scene, camera);
引用glsl文件 用于将 GLSL 文件转换为 JavaScript 代码。它允许您在 Vue、React 或其他支持 Vite 的框架中使用 GLSL 着色 二选一即可:
vite-plugin-glslify
很久没更新了
vite-plugin-glsl
还在持续更新1 2 npm install vite-plugin-glslify -d npm install vite-plugin-glsl -d
安装好glsl后需要在vite.config.js中配置1 2 3 4 5 6 import glsl from 'vite-plugin-glsl' export default defineConfig ({ plugins : [ glsl () ] })
在vue文件中引用glsl 着色器材料中失效的属性 map alphaMap color etc 需要在glsl中处理
1 2 3 4 5 6 7 8 9 10 11 12 <script setup lang ="ts" > import vertexShader from './vertexShader.glsl' import fragmentShader from './fragmentShader.glsl' const material = new Three .ShaderMaterial ({ vertexShader : vertexShader, fragmentShader : fragmentShader, uniforms : { color : { value : new Three .Color (0xff0000 ) } } }); </script >
插件 代码插件
vertex.glsl 1 gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
这个表达式首先通过modelViewMatrix将物体的位置从对象空间变换到视图空间。
然后,通过projectionMatrix将视图空间中的位置变换到裁剪空间。
最终得到的gl_Position是一个四维向量,在光栅化阶段,OpenGL会使用这个向量的x, y, z分量进行透视除法(使用w分量),从而得到归一化设备坐标(Normalized Device Coordinates, NDC),这是进行光栅化和片段着色之前的一个重要步骤。
常量 常量可以不声明直接使用
``
Attributes Attributes 主要用于顶点着色器,并且通常包含与每个顶点相关的数据,例如顶点位置、法线、纹理坐标等。在Three.js中,常见的attributes包括:
position:顶点的位置(通常在模型空间中)。
normal:顶点的法线向量(用于光照计算)。
uv:顶点的纹理坐标。
color:顶点的颜色。
Uniforms 是传递给着色器的常量值,它们在着色器程序执行期间不会改变,但对于每一帧或每一次绘制调用,它们的值可以从CPU端设置。在Three.js中,你可以使用许多内置的uniforms,也可以定义自己的uniforms。一些常见的内置uniforms包括:
projectionMatrix:投影矩阵(用于将视图空间坐标转换为裁剪空间坐标)。
modelViewMatrix:模型视图矩阵(用于将模型空间坐标转换为视图空间坐标)。
normalMatrix:法线矩阵(用于变换法线向量)。
cameraPosition:相机在世界空间中的位置。
resolution:渲染目标的分辨率(宽度和高度)。
time:当前时间(通常用于动画效果)。
变量
uniform 接收js(uniforms)传递来的参数
attribute 接收着色器的attribute属性
varying 声明变量传递给片段着色器(用于从顶点着色器传递数据到片段着色器。它们在顶点着色器中被写入,并在片段着色器中被读取。Varyings在光栅化过程中会被插值,以便为每个片段生成一个值。)
数据类型
vec2 vec3 vec4 矢量类型
mat2 mat3 mat4 矩阵类型
float 浮点类型
void 空类型
bool 布尔类型
sampler2D 纹理类型
attribute 顶点属性
强类型语言 类似于C预言
1 2 3 4 5 6 7 8 9 10 11 12 const loader = new Three .TextureLoader () const material = new Three .ShaderMaterial ({ vertexShader : vertexShader, fragmentShader : fragmentShader, uniforms : { color : { value : new Three .Color (0xff00ff ) }, uFren : { value : 2 }, uTime : { value : 0 }, uTexture : { value : loader.load (new URL ('@/assets/us.jpg' , import .meta .url ).href ) } } });
顶点着色器代码 vertex.glsl
1 2 3 4 5 6 7 8 9 10 11 12 uniform vec3 color; uniform float uFren; uniform float uTime; attribute float aRandom; varying vec2 vuv; varying float z;void main () { z = sin (position.x * uFren + uTime) * 0.3 ; z += sin (position.y * uFren + uTime) * 0.3 ; gl_Position = projectionMatrix * modelViewMatrix * vec4( position.x, position.y, z, 1.0 ); vuv = uv; }
片段着色器代码 fragment.glsl
1 2 3 4 5 6 7 8 9 10 uniform vec3 color; uniform sampler2D uTexture; varying vec2 vuv; varying float z;void main () { gl_FragColor = vec4(color , 1.0 ); vec4 texColor = texture2D(uTexture, vuv); texColor.rgb *= z * 1.0 + 1.0 ; gl_FragColor = texColor; }
fragment 片段着色器 例子
mod(x, y) 返回 x / y 的余数
step(a, b) 返回 a < b ? 1.0 : 0.0
clamp(a, b, c) 返回 max(a, min(b, c))
max(a,b) 返回最大值
min(a,b) 返回最小值
abs(a) 返回绝对值
length(vuv) 返回向量长度
distance(vuv,vec2(0.0,0.0)) 返回两点距离
atan(vuv.x,vuv.y) 返回反正切值
mix(a,b,c) 混合两种颜色 a第一种 b第二种 c渲染的向量位置
公用部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #define PI 3.1415926535897932384626433832795 varying vec2 vUv;float random (vec2 st) { return fract(sin (dot(st.xy, vec2(12.9898 ,78.233 ))) * 43758.5453123 ); } vec2 rotate (vec2 uv, float rotation, vec2 mid) { return vec2( cos (rotation) * (uv.x - mid.x) + sin (rotation) * (uv.y - mid.y) + mid.x, cos (rotation) * (uv.y - mid.y) - sin (rotation) * (uv.x - mid.x) + mid.y ); } vec2 fade (vec2 t) { return t*t*t*(t*(t*6.0 -15.0 )+10.0 ); } vec4 permute (vec4 x) { return mod(((x*34.0 )+1.0 )*x, 289.0 ); }float cnoise (vec2 P) { vec4 Pi = floor (P.xyxy) + vec4(0.0 , 0.0 , 1.0 , 1.0 ); vec4 Pf = fract(P.xyxy) - vec4(0.0 , 0.0 , 1.0 , 1.0 ); Pi = mod(Pi, 289.0 ); vec4 ix = Pi.xzxz; vec4 iy = Pi.yyww; vec4 fx = Pf.xzxz; vec4 fy = Pf.yyww; vec4 i = permute(permute(ix) + iy); vec4 gx = 2.0 * fract(i * 0.0243902439 ) - 1.0 ; vec4 gy = abs (gx) - 0.5 ; vec4 tx = floor (gx + 0.5 ); gx = gx - tx; vec2 g00 = vec2(gx.x,gy.x); vec2 g10 = vec2(gx.y,gy.y); vec2 g01 = vec2(gx.z,gy.z); vec2 g11 = vec2(gx.w,gy.w); vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)); g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w; float n00 = dot(g00, vec2(fx.x, fy.x)); float n10 = dot(g10, vec2(fx.y, fy.y)); float n01 = dot(g01, vec2(fx.z, fy.z)); float n11 = dot(g11, vec2(fx.w, fy.w)); vec2 fade_xy = fade(Pf.xy); vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x); float n_xy = mix(n_x.x, n_x.y, fade_xy.y); return 2.3 * n_xy; }
使用demo
1 2 gl_FragColor = vec4(vUv, 1.0 , 1.0 );
1 2 gl_FragColor = vec4(vUv, 0.0 , 1.0 );
1 2 float strength = vUv.x;
1 2 float strength = vUv.y;
1 2 float strength = 1.0 - vUv.y;
1 2 float strength = vUv.y * 10.0 ;
1 2 float strength = mod(vUv.y * 10.0 , 1.0 );
1 2 3 float strength = mod(vUv.y * 10.0 , 1.0 ); strength = step(0.5 , strength);
1 2 3 float strength = mod(vUv.y * 10.0 , 1.0 ); strength = step(0.8 , strength);
1 2 3 float strength = mod(vUv.x * 10.0 , 1.0 ); strength = step(0.8 , strength);
1 2 3 4 float strength = step(0.8 , mod(vUv.x * 10.0 , 1.0 )); strength += step(0.8 , mod(vUv.y * 10.0 , 1.0 )); strength = clamp(strength, 0.0 , 1.0 );
1 2 3 float strength = step(0.8 , mod(vUv.x * 10.0 , 1.0 )); strength *= step(0.8 , mod(vUv.y * 10.0 , 1.0 ));
1 2 3 float strength = step(0.4 , mod(vUv.x * 10.0 , 1.0 )); strength *= step(0.8 , mod(vUv.y * 10.0 , 1.0 ));
1 2 3 4 5 float barX = step(0.4 , mod(vUv.x * 10.0 , 1.0 )) * step(0.8 , mod(vUv.y * 10.0 , 1.0 ));float barY = step(0.8 , mod(vUv.x * 10.0 , 1.0 )) * step(0.4 , mod(vUv.y * 10.0 , 1.0 ));float strength = barX + barY; strength = clamp(strength, 0.0 , 1.0 );
1 2 3 4 5 float barX = step(0.4 , mod(vUv.x * 10.0 - 0.2 , 1.0 )) * step(0.8 , mod(vUv.y * 10.0 , 1.0 ));float barY = step(0.8 , mod(vUv.x * 10.0 , 1.0 )) * step(0.4 , mod(vUv.y * 10.0 - 0.2 , 1.0 ));float strength = barX + barY; strength = clamp(strength, 0.0 , 1.0 );
1 2 float strength = abs (vUv.x - 0.5 );
1 2 float strength = min(abs (vUv.x - 0.5 ), abs (vUv.y - 0.5 ));
1 2 float strength = max(abs (vUv.x - 0.5 ), abs (vUv.y - 0.5 ));
1 2 float strength = step(0.2 , max(abs (vUv.x - 0.5 ), abs (vUv.y - 0.5 )));
1 2 3 float strength = step(0.2 , max(abs (vUv.x - 0.5 ), abs (vUv.y - 0.5 ))); strength *= 1.0 - step(0.25 , max(abs (vUv.x - 0.5 ), abs (vUv.y - 0.5 )));
1 2 float strength = floor (vUv.x * 10.0 ) / 10.0 ;
1 2 float strength = floor (vUv.x * 10.0 ) / 10.0 * floor (vUv.y * 10.0 ) / 10.0 ;
1 2 float strength = random(vUv);
1 2 3 vec2 gridUv = vec2(floor (vUv.x * 10.0 ) / 10.0 , floor (vUv.y * 10.0 ) / 10.0 );float strength = random(gridUv);
1 2 3 vec2 gridUv = vec2(floor (vUv.x * 10.0 ) / 10.0 , floor ((vUv.y + vUv.x * 0.5 ) * 10.0 ) / 10.0 );float strength = random(gridUv);
1 2 float strength = length(vUv);
1 2 float strength = distance(vUv, vec2(0.5 ));
1 2 float strength = 1.0 - distance(vUv, vec2(0.5 ));
1 2 float strength = 0.015 / (distance(vUv, vec2(0.5 )));
1 2 float strength = 0.15 / (distance(vec2(vUv.x, (vUv.y - 0.5 ) * 5.0 + 0.5 ), vec2(0.5 )));
1 2 3 float strength = 0.15 / (distance(vec2(vUv.x, (vUv.y - 0.5 ) * 5.0 + 0.5 ), vec2(0.5 ))); strength *= 0.15 / (distance(vec2(vUv.y, (vUv.x - 0.5 ) * 5.0 + 0.5 ), vec2(0.5 )));
1 2 3 4 vec2 rotatedUv = rotate(vUv, PI * 0.25 , vec2(0.5 ));float strength = 0.15 / (distance(vec2(rotatedUv.x, (rotatedUv.y - 0.5 ) * 5.0 + 0.5 ), vec2(0.5 ))); strength *= 0.15 / (distance(vec2(rotatedUv.y, (rotatedUv.x - 0.5 ) * 5.0 + 0.5 ), vec2(0.5 )));
1 2 float strength = step(0.5 , distance(vUv, vec2(0.5 )) + 0.25 );
1 2 float strength = abs (distance(vUv, vec2(0.5 )) - 0.25 );
1 2 float strength = step(0.01 , abs (distance(vUv, vec2(0.5 )) - 0.25 ));
1 2 float strength = 1.0 - step(0.01 , abs (distance(vUv, vec2(0.5 )) - 0.25 ));
1 2 3 4 5 6 vec2 wavedUv = vec2( vUv.x, vUv.y + sin (vUv.x * 30.0 ) * 0.1 );float strength = 1.0 - step(0.01 , abs (distance(wavedUv, vec2(0.5 )) - 0.25 ));
1 2 3 4 5 6 vec2 wavedUv = vec2( vUv.x + sin (vUv.y * 30.0 ) * 0.1 , vUv.y + sin (vUv.x * 30.0 ) * 0.1 );float strength = 1.0 - step(0.01 , abs (distance(wavedUv, vec2(0.5 )) - 0.25 ));
1 2 3 4 5 6 vec2 wavedUv = vec2( vUv.x + sin (vUv.y * 100.0 ) * 0.1 , vUv.y + sin (vUv.x * 100.0 ) * 0.1 );float strength = 1.0 - step(0.01 , abs (distance(wavedUv, vec2(0.5 )) - 0.25 ));
1 2 3 float angle = atan (vUv.x, vUv.y);float strength = angle;
1 2 3 float angle = atan (vUv.x - 0.5 , vUv.y - 0.5 );float strength = angle;
1 2 3 float angle = atan (vUv.x - 0.5 , vUv.y - 0.5 ) / (PI * 2.0 ) + 0.5 ;float strength = angle;
1 2 3 float angle = atan (vUv.x - 0.5 , vUv.y - 0.5 ) / (PI * 2.0 ) + 0.5 ;float strength = mod(angle * 20.0 , 1.0 );
1 2 3 float angle = atan (vUv.x - 0.5 , vUv.y - 0.5 ) / (PI * 2.0 ) + 0.5 ;float strength = sin (angle * 100.0 );
1 2 3 4 float angle = atan (vUv.x - 0.5 , vUv.y - 0.5 ) / (PI * 2.0 ) + 0.5 ;float radius = 0.25 + sin (angle * 100.0 ) * 0.02 ;float strength = 1.0 - step(0.01 , abs (distance(vUv, vec2(0.5 )) - radius));
1 2 float strength = cnoise(vUv * 10.0 );
1 2 float strength = step(0.0 , cnoise(vUv * 10.0 ));
1 2 float strength = 1.0 - abs (cnoise(vUv * 10.0 ));
1 2 float strength = sin (cnoise(vUv * 10.0 ) * 20.0 );
1 2 float strength = step(0.9 , sin (cnoise(vUv * 10.0 ) * 20.0 ));
1 gl_FragColor = vec4(vec3(strength), 1.0 );
1 2 3 4 5 vec3 blackColor = vec3(0.0 ); vec3 uvColor = vec3(vUv, 1.0 ); vec3 mixedColor = mix(blackColor, uvColor, strength); gl_FragColor = vec4(mixedColor, 1.0 );
11添加2d标签 <div :ref='index' :id='index'>标注</div>
1 2 3 4 5 6 7 8 9 10 11 import {CSS2DObject } from 'three/examples/jsm/renderers/CSS2DObject.js' import {scene} from './three/index.js' mounted ( ){ var div = this .$refs [this .index ] var label = new CSS2DObject (div) div.style .pointerEvents = 'none' label.position .set (this .x ,this .y ,this .z ) scene.add (label) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import {CSS2DRenderder } from 'three/examples/jsm/renderers/CSS2DRenderder' var CSS2LabelRenderder = new CSS2DRenderder ()CSS2LabelRenderder .setSize (500 ,500 )CSS2LabelRenderder .domElement .style .position = 'absolute' CSS2LabelRenderder .domElement .style .left = '0px' CSS2LabelRenderder .domElement .style .top = '0px' CSS2LabelRenderder .domElement .style .pointerEvents = 'none' document .body .appendChild (CSS2LabelRenderder .domElement )export {CSS2LabelRenderer }import {CSS2LabelRenderer } from 'CSS2DRenderer.js' render.render (scene,camera)CSS2LabelRenderer .render (scene,camera)