├── .gitignore ├── demo ├── lineSketch │ ├── index.css │ ├── index.html │ ├── main.js │ ├── models │ │ └── gpu.glb │ ├── readme.md │ ├── textures │ │ ├── background.png │ │ └── linemap.image │ └── utils │ │ ├── app.js │ │ ├── clip.js │ │ ├── effect.js │ │ ├── index.js │ │ ├── kit.js │ │ ├── load.js │ │ ├── outline.js │ │ ├── pick.js │ │ ├── sketch.js │ │ └── toast.js └── minecraft │ ├── buglist.md │ ├── font │ └── unifont.ttf │ ├── images │ ├── apple.png │ ├── btn.png │ ├── food.png │ ├── hp.png │ ├── level.png │ ├── logo.png │ └── menu_item.png │ ├── index.css │ ├── index.html │ ├── index.js │ ├── models │ ├── bench.glb │ ├── block.glb │ ├── chest.glb │ ├── cloud.glb │ ├── cloud2.glb │ ├── cloud3.glb │ ├── cloud4.glb │ ├── cloud5.glb │ ├── cloud6.glb │ ├── custom_house.glb │ ├── flower.glb │ ├── fox.glb │ ├── grass-block.glb │ ├── grass.glb │ ├── house-v1.glb │ ├── lantern.glb │ ├── static_steve.glb │ ├── steve.glb │ ├── torch.glb │ ├── tree.glb │ ├── village-v1.glb │ ├── village.glb │ ├── village2.glb │ ├── wolf.glb │ └── wood_shop.glb │ ├── move.drawio │ └── texture │ ├── floor.png │ ├── floor2.png │ └── sun.png ├── documents ├── QQ_1736198772143.png ├── image-1.png ├── image-2.png ├── image-3.png ├── image-4.png ├── image-5.png ├── image-6.png ├── image-7.png ├── image-8.png ├── image-9.png ├── image.png ├── models │ ├── index.html │ ├── index.js │ └── public │ │ ├── scene.bin │ │ ├── scene.gltf │ │ └── textures │ │ └── lambert5SG_emissive.jpeg ├── start │ ├── index.html │ ├── index.js │ └── 创建场景.md ├── 光源.md ├── 几何体&顶点&材质 │ ├── index.html │ ├── index.js │ └── 几何体_顶点_索引_面_材质.md ├── 创建文字 │ ├── helvetiker_regular.typeface.json │ ├── index.html │ ├── index.js │ └── 创建文字.md ├── 动画.md ├── 后期处理effectComposer │ ├── index.js │ └── readme.md ├── 图元.md ├── 场景图sceneGraph │ ├── class_AxisGridHelper.js │ ├── image-1.png │ ├── image-2.png │ ├── image.png │ ├── index.html │ ├── index.js │ └── 场景图.md ├── 归纳文档.md ├── 材质.md ├── 画线 │ ├── index.html │ └── index.js ├── 相机camera │ ├── image.png │ ├── index.html │ ├── index.js │ └── 相机.md ├── 着色器shader │ ├── README.md │ ├── base │ │ ├── index.html │ │ └── index.js │ ├── circle │ │ ├── index.html │ │ └── index.js │ └── index.html ├── 纹理.md ├── 贴图map │ ├── index.html │ ├── index.js │ └── 贴图.md ├── 阴影shadow │ ├── image-1.png │ ├── image-2.png │ ├── image-3.png │ ├── image.png │ ├── index.html │ ├── index.js │ └── 阴影.md └── 雾fog │ ├── image.png │ ├── index.html │ ├── index.js │ └── 雾.md ├── index.html ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/lineSketch/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /demo/lineSketch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | linkSketch demo 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/lineSketch/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js"; 3 | 4 | import { 5 | loadModel, 6 | loadTexture, 7 | getSketchInfos, 8 | createSketch, 9 | App, 10 | Clip, 11 | } from "./utils/index.js"; 12 | 13 | const app = new App(document.getElementById("container")); 14 | 15 | app.animate(); 16 | 17 | let clipping; 18 | let clipping2; 19 | // 加载模型 20 | loadModel("gpu.glb").then((glb) => { 21 | const model = glb.scene; 22 | model.scale.set(2, 2, 2); 23 | model.position.x = 2; 24 | app.scene.add(model); 25 | clipping = new Clip(app.scene, model, app.renderer, { outline: true }); 26 | 27 | // 根据模型生成边缘轮廓 28 | { 29 | const meshs = []; 30 | const fanMeshs = []; 31 | const fanName = [ 32 | "left_darker_Black_Plastic_001_0", 33 | "Plane_darker_Black_Plastic_001_0", 34 | "Plane044_darker_Black_Plastic_001_0", 35 | "left002_darker_Black_Plastic_001_0", 36 | ]; 37 | 38 | model.traverse((item) => { 39 | if (item.isMesh) { 40 | fanName.includes(item.name) ? fanMeshs.push(item) : meshs.push(item); 41 | } 42 | }); 43 | 44 | const sketchInfos = getSketchInfos(meshs); 45 | const _sketchInfos = getSketchInfos(fanMeshs); 46 | const sketch = createSketch(sketchInfos); 47 | const _sketch = createSketch(_sketchInfos, "lightgreen", 0.8); 48 | 49 | // app.scene.add(sketch); 50 | // app.scene.add(_sketch); 51 | 52 | const sketchGroup = new THREE.Group(); 53 | sketchGroup.add(sketch); 54 | sketchGroup.add(_sketch); 55 | app.scene.add(sketchGroup); 56 | 57 | clipping2 = new Clip(app.scene, sketchGroup, app.renderer); 58 | clipping2.invert(); 59 | 60 | const box = new THREE.Box3().setFromObject(sketchGroup); 61 | const helper = new THREE.Box3Helper(box, 0xffff00); 62 | app.scene.add(helper); 63 | 64 | const helpers = new THREE.Group(); 65 | helpers.add(new THREE.PlaneHelper(clipping.clipPlanes[0], 2, 0xff0000)); 66 | helpers.visible = true; 67 | app.scene.add(helpers); 68 | 69 | // 旋转扇叶 70 | setInterval(() => { 71 | _sketch.children.forEach((item) => { 72 | item.rotation.z += 0.2; 73 | }); 74 | }, 16); 75 | } 76 | }); 77 | 78 | // 加载背景贴图 79 | loadTexture("background.png").then((texture) => { 80 | texture.colorSpace = THREE.SRGBColorSpace; 81 | app.scene.background = texture; 82 | }); 83 | 84 | // 添加光源 85 | { 86 | const ambientLight = new THREE.AmbientLight("#2f2f2f", 40); 87 | 88 | const directionalLight = new THREE.DirectionalLight(0xffffff, 5); 89 | directionalLight.position.set(1, 2, 5); 90 | 91 | app.scene.add(directionalLight); 92 | app.scene.add(ambientLight); 93 | } 94 | 95 | // 设置剪裁动画 96 | app.container.addEventListener("pointerdown", () => { 97 | clipping.cover(); 98 | clipping2.cover(); 99 | }); 100 | // 抬起播放恢复动画 101 | app.container.addEventListener("pointerup", () => { 102 | clipping.restore(); 103 | clipping2.restore(); 104 | }); 105 | -------------------------------------------------------------------------------- /demo/lineSketch/models/gpu.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/lineSketch/models/gpu.glb -------------------------------------------------------------------------------- /demo/lineSketch/readme.md: -------------------------------------------------------------------------------- 1 | ## 根据 mesh 网格 创建 线型轮廓 LineSegments + EdgesGeometry 2 | 3 | ```js 4 | import * as THREE from "three"; 5 | 6 | function getSketchInfos(meshs) { 7 | const sketchInfos = meshs.map((mesh) => { 8 | if (mesh.isMesh) { 9 | // 获取四元数 10 | const quaternion = new THREE.Quaternion(); 11 | mesh.getWorldQuaternion(quaternion); 12 | // 获取世界坐标 13 | const worldPos = new THREE.Vector3(); 14 | mesh.getWorldPosition(worldPos); 15 | // 获取缩放比例 16 | const worldScale = new THREE.Vector3(); 17 | mesh.getWorldScale(worldScale); 18 | 19 | return { 20 | geometry: mesh.geometry, 21 | quaternion, 22 | worldPos, 23 | worldScale, 24 | }; 25 | } 26 | }); 27 | return sketchInfos; 28 | } 29 | 30 | function createLine(material) { 31 | return function (sketchInfo, angle = 1) { 32 | const { geometry, quaternion, worldPos, worldScale } = sketchInfo; 33 | const edges = new THREE.EdgesGeometry(geometry, angle); 34 | const line = new THREE.LineSegments(edges); 35 | line.material = material; 36 | line.quaternion.copy(quaternion); 37 | line.position.copy(worldPos); 38 | line.scale.copy(worldScale); 39 | return line; 40 | }; 41 | } 42 | 43 | function createSketch(sketchInfos, color = "#0fb2fb", opacity = 1) { 44 | const material = new THREE.LineBasicMaterial({ 45 | color: new THREE.Color(color), 46 | transparent: true, 47 | opacity, 48 | }); 49 | const createLineWithMaterial = createLine(material); 50 | 51 | const sketch = new THREE.Group(); 52 | sketchInfos.forEach((sketchInfo) => { 53 | // 以模型顶点信息创建线条 54 | const line = createLineWithMaterial(sketchInfo); 55 | sketch.add(line); 56 | }); 57 | return sketch; 58 | } 59 | 60 | export { getSketchInfos, createSketch }; 61 | ``` 62 | 63 | ## 封装后期处理 64 | 65 | 后续优化灵活性 66 | 67 | ```js 68 | import * as THREE from "three"; 69 | import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; 70 | import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; 71 | import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; 72 | import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; 73 | 74 | function setupComposer(scene, camera, renderer) { 75 | const renderPass = new RenderPass(scene, camera); 76 | const params = { 77 | threshold: 0.2, 78 | strength: 0.35, 79 | radius: 0, 80 | }; 81 | const bloomPass = new UnrealBloomPass( 82 | new THREE.Vector2(window.innerWidth, window.innerHeight) 83 | ); 84 | bloomPass.threshold = params.threshold; 85 | bloomPass.strength = params.strength; 86 | bloomPass.radius = params.radius; 87 | 88 | const outputPass = new OutputPass(); 89 | 90 | const composer = new EffectComposer(renderer); 91 | composer.addPass(renderPass); 92 | composer.addPass(bloomPass); 93 | composer.addPass(outputPass); 94 | 95 | return composer; 96 | } 97 | 98 | export { setupComposer }; 99 | ``` 100 | 101 | ## 根据鼠标指向拾取 mesh 102 | 103 | ```js 104 | class PickHelper { 105 | constructor(canvas) { 106 | this.canvas = canvas; 107 | this.raycaster = new THREE.Raycaster(); 108 | this.pickedObject = null; 109 | this.pickedObjectSavedColor = 0; 110 | } 111 | pick(scene, camera, time) { 112 | // 发出射线 113 | this.raycaster.setFromCamera(this.pickPosition, camera); 114 | // 获取与射线相交的对象 115 | const intersectedObjects = this.raycaster.intersectObjects(scene.children); 116 | if (intersectedObjects.length) { 117 | // 找到第一个对象,它是离鼠标最近的对象 118 | this.pickedObject = intersectedObjects[0].object; 119 | 120 | this.pickedObjectSavedColor = getMaterialColor(material); 121 | 122 | setMaterialColor(material, (time * 8) % 2 > 1 ? 0xe0ffe0 : 0x90ee90); 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | ## clipping 效果 129 | 130 | 平面(Plane)https://threejs.org/docs/index.html?q=plane#api/zh/math/Plane ,在三维空间中无限延伸的二维平面 131 | 132 | Plane( normal : Vector3, constant : Float ) 133 | normal - (可选参数) 定义单位长度的平面法向量 Vector3。默认值为 (1, 0, 0)。 134 | constant - (可选参数) 从原点到平面的有符号距离。 默认值为 0. 135 | 136 | 增加 helper 137 | 138 | ```js 139 | // helpers 140 | const helpers = new THREE.Group(); 141 | helpers.add(new THREE.PlaneHelper(clipPlanes[0], 2, 0xff0000)); 142 | helpers.add(new THREE.PlaneHelper(clipPlanes[1], 2, 0x00ff00)); 143 | helpers.add(new THREE.PlaneHelper(clipPlanes[2], 2, 0x0000ff)); 144 | helpers.visible = false; 145 | scene.add(helpers); 146 | ``` 147 | 148 | ```js 149 | this.renderer.localClippingEnabled = true; 150 | 151 | const plane = new THREE.Plane(new THREE.Vector3(2, 0, 0), x_range[1]); 152 | this.clipPlanes.push(plane); 153 | 154 | const material = new THREE.MeshPhongMaterial({ 155 | color: new THREE.Color().setHSL( 156 | Math.random(), 157 | 0.5, 158 | 0.5, 159 | THREE.SRGBColorSpace 160 | ), 161 | side: THREE.DoubleSide, 162 | clippingPlanes: this.clipPlanes, // 指定材质的剪裁面,如果是模型material需要traverse迭代 163 | clipIntersection: params.clipIntersection, 164 | alphaToCoverage: true, 165 | }); 166 | // 获取模型边界 用于灵活设置constant 167 | function computedRange(model) { 168 | const box = new THREE.Box3().setFromObject(model); 169 | const max = box.max; 170 | const min = box.min; 171 | 172 | return [min.x, max.x]; 173 | } 174 | ``` 175 | 176 | `.clipIntersection` 更改剪裁平面的行为,以便仅剪切其交叉点,而不是它们的并集 177 | 178 | `.clippingPlanes` 用户定义的剪裁平面,在世界空间中指定为 THREE.Plane 对象。这些平面适用于所有使用此材质的对象 179 | 180 | `clipPlanes[ j ].constant = value;` 修改剪裁平台 181 | 182 | 示例: 183 | 184 | https://threejs.org/examples/#webgl_clipping_intersection 185 | 186 | removeFromParent 删除原有模型 187 | 188 | TODOList: 189 | 190 | 1. done 支持高亮指定层级 191 | 2. done 支持获取到鼠标指向的模型 192 | 3. 在模型上添加精灵点 193 | 4. 点击精灵点移动摄像机聚焦,展示提示文字 194 | 5. 添加 gui 195 | 6. 线条的流转和渐变色 196 | 7. 添加模型参数描述 197 | 8. edge 线条更加明亮 198 | 199 | `updateMatrixWorld`非常重要,位移 旋转 缩放后一定得调用一次 200 | .applyMatrix4 ( matrix : Matrix4 ) : undefined 201 | 对当前物体应用这个变换矩阵,并更新物体的位置、旋转和缩放。 202 | 203 | ## 性能相关 204 | 205 | ```js 206 | class clip { 207 | this.temp = xxx; 208 | 209 | function clip () { 210 | this.temp.xx = xx; 211 | } 212 | } 213 | ``` 214 | 215 | 为什么是`temp`不是`const` 216 | 217 | 避免内存抖动: 218 | 在渲染循环中,这些方法每帧都会被调用 219 | 如果每帧都创建和销毁对象,会导致频繁的垃圾回收 220 | 频繁的垃圾回收会造成性能抖动 221 | 222 | ## plane 与模型的坐标问题 223 | 224 | ```js 225 | // 假设模型被缩放了2倍并移动到(10,0,0) 226 | model.scale.set(2, 2, 2); 227 | model.position.set(10, 0, 0); 228 | 229 | // 切割平面在世界空间中 230 | clippingPlane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0); 231 | 232 | // 需要将平面转换到局部空间才能正确计算交点 233 | // 否则,计算出的交点位置会出错 234 | ``` 235 | 236 | ## 通过欧拉角与四元数 灵活的更新物体位置 237 | 238 | ```js 239 | // 基于欧拉角改变向量位置 240 | export const euler2Matrix = ( 241 | originalVector: Vector3, 242 | eulerAngles: Euler, 243 | distance = 1 244 | ) => { 245 | // 1. 将Euler转换为Quaternion 246 | const quaternion = new Quaternion().setFromEuler(eulerAngles); 247 | 248 | // 2. 创建一个单位方向向量(例如沿Z轴) 249 | const directionVector = new Vector3(0, 0, 1); 250 | 251 | // 3. 使用Quaternion旋转方向向量 252 | directionVector.applyQuaternion(quaternion); 253 | 254 | // 4. 将旋转后的方向向量乘以距离,并将其加到原始向量上 255 | const movedVector = originalVector 256 | .clone() 257 | .addScaledVector(directionVector, distance); 258 | 259 | return movedVector; 260 | }; 261 | ``` 262 | 263 | 我们的目标是让物体沿Z轴发生一定变化,但此时物体可能有自己的旋转角度,结合四元数算出准确更新方式。 264 | 265 | 比如我原地跳高,和向前跳远,Y轴在计算时 需要物体提供一个欧拉角转换为四元数 再将Y轴的变化通过四元数转换为准确的(XY轴变化),就像跳远是+X+Y 并不是只有+Y 266 | -------------------------------------------------------------------------------- /demo/lineSketch/textures/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/lineSketch/textures/background.png -------------------------------------------------------------------------------- /demo/lineSketch/textures/linemap.image: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/lineSketch/textures/linemap.image -------------------------------------------------------------------------------- /demo/lineSketch/utils/app.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | import { setupComposer, PickHelper } from "./index"; 4 | 5 | class App { 6 | scene = null; 7 | camera = null; 8 | renderer = null; 9 | controls = null; 10 | composer = null; 11 | picker = null; 12 | callbacks = []; 13 | 14 | // todo:接收scene、camera、renderer...而非自己生成 15 | constructor(container) { 16 | this.container = container; 17 | container && this.init(); 18 | } 19 | 20 | init() { 21 | // 初始化场景 22 | this.scene = new THREE.Scene(); 23 | this.scene.background = new THREE.Color("#212121"); 24 | this.scene.colorSpace = THREE.SRGBColorSpace; 25 | 26 | // 初始化相机 27 | this.camera = new THREE.PerspectiveCamera( 28 | 75, 29 | window.innerWidth / window.innerHeight, 30 | 0.1, 31 | 1000 32 | ); 33 | this.camera.position.z = 2.5; 34 | this.camera.position.y = 2; 35 | 36 | // 初始化渲染器 37 | this.renderer = new THREE.WebGLRenderer({ 38 | canvas: this.container, 39 | antialias: true, 40 | }); 41 | this.renderer.setSize(window.innerWidth, window.innerHeight); 42 | this.renderer.setPixelRatio(window.devicePixelRatio); 43 | this.renderer.outputEncoding = THREE.sRGBEncoding; 44 | 45 | // 创建轨道控制器 46 | this.controls = new OrbitControls(this.camera, this.container); 47 | this.controls.enableDamping = true; 48 | window.addEventListener("resize", this.onResize.bind(this)); 49 | 50 | // 注册composer 51 | this.composer = setupComposer(this.scene, this.camera, this.renderer); 52 | // 创建拾取器picker 53 | this.picker = new PickHelper(this.container); 54 | } 55 | 56 | onResize() { 57 | this.camera.aspect = window.innerWidth / window.innerHeight; 58 | this.camera.updateProjectionMatrix(); 59 | this.renderer.setSize(window.innerWidth, window.innerHeight); 60 | this.composer && 61 | this.composer.setSize(window.innerWidth, window.innerHeight); 62 | } 63 | 64 | addCallback(callback) { 65 | this.callbacks.push(callback); 66 | } 67 | removeCallback(callback) { 68 | this.callbacks = this.callbacks.filter((cb) => cb !== callback); 69 | } 70 | 71 | animate() { 72 | requestAnimationFrame(this.animate.bind(this)); 73 | this.controls.update(); 74 | // this.picker.pick(this.scene, this.camera); 75 | this.composer 76 | ? this.composer.render() 77 | : this.renderer.render(this.scene, this.camera); 78 | // this.renderer.render(this.scene, this.camera); 79 | this.callbacks.forEach((callback) => callback()); 80 | } 81 | } 82 | 83 | export { App }; 84 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/clip.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import OutlineGenerator from "./outline"; 3 | 4 | const ANIMATE_TIME = 100; 5 | 6 | class Clip { 7 | isAnimating = false; 8 | animationId = null; 9 | scene = null; 10 | model = null; 11 | clipPlanes = []; 12 | renderer = null; 13 | edgeColor = 0x80deea; 14 | range = [0, 0]; 15 | step = 0; 16 | options = {}; 17 | 18 | constructor(scene, model, renderer, options = {}) { 19 | this.scene = scene; 20 | this.model = model; 21 | this.renderer = renderer; 22 | this.options = options; 23 | this.renderer.localClippingEnabled = true; 24 | this.edgeColor = options.color || 0x80deea; 25 | this.init(); 26 | } 27 | 28 | animate(isReStore, timeStamp, lastTime = 0) { 29 | if (timeStamp - lastTime > 25) { 30 | lastTime = timeStamp; 31 | const plane = this.clipPlanes[0]; 32 | const isRevert = plane.normal.x < 0; 33 | 34 | const max = this.range[1]; 35 | const min = this.range[0]; 36 | this.isAnimating = true; 37 | 38 | let result = 39 | this.clipPlanes[0].constant - (2 * !!isReStore - 1) * this.step; 40 | 41 | if (isReStore) { 42 | if (isRevert) result = result < min ? min : result; 43 | else result = result > min ? min : result; 44 | } else { 45 | if (isRevert) result = result > max ? max : result; 46 | else result = result < max ? max : result; 47 | } 48 | 49 | this.setConstant(result); 50 | this.outline && this.outline.updateOutLines(plane); 51 | if (result === this.range[isRevert ? +isReStore : +!isReStore]) 52 | return this.stopAnimate(); 53 | } 54 | this.animationId = requestAnimationFrame((now) => 55 | this.animate(isReStore, now, lastTime) 56 | ); 57 | } 58 | 59 | stopAnimate() { 60 | this.animationId && cancelAnimationFrame(this.animationId); 61 | this.animationId = null; 62 | this.isAnimating = false; 63 | } 64 | 65 | invert() { 66 | const plane = this.clipPlanes[0]; 67 | this.range = [-this.range[0], -this.range[1]]; 68 | // this.range = [-this.range[1], -this.range[0]]; 69 | 70 | plane.negate(); 71 | this.step = -this.step; 72 | } 73 | 74 | cover() { 75 | this.stopAnimate(); 76 | requestAnimationFrame((timeStamp) => this.animate(false, timeStamp)); 77 | } 78 | 79 | restore() { 80 | this.stopAnimate(); 81 | requestAnimationFrame((timeStamp) => this.animate(true, timeStamp)); 82 | } 83 | 84 | setConstant(constant) { 85 | this.clipPlanes[0].constant = constant; 86 | } 87 | 88 | init() { 89 | if (!this.model) return; 90 | 91 | // 将剪裁面与模型重叠的轮廓添加到场景中 92 | if (this.options.outline) { 93 | this.outline = new OutlineGenerator(this.model); 94 | this.scene.add(this.outline.outlines); 95 | } 96 | 97 | // 获取模型边界 98 | const x_range = computedRange(this.model); 99 | this.range = x_range; 100 | // 计算每步(帧)更新的距离 101 | this.step = (x_range[1] - x_range[0]) / ANIMATE_TIME; 102 | 103 | // 生成剪裁平面 104 | const plane = new THREE.Plane(new THREE.Vector3(1, 0, 0), x_range[0]); 105 | this.clipPlanes.push(plane); 106 | 107 | // todo:优化写法 108 | // plane.applyMatrix4(this.model.matrixWorld); 109 | 110 | // 修改模型材质设置剪裁面 111 | this.model.traverse((child) => { 112 | if (child.isMesh || child.isLineSegments) { 113 | child.material.clippingPlanes = this.clipPlanes; 114 | child.material.clipShadows = true; 115 | } 116 | }); 117 | } 118 | } 119 | 120 | function computedRange(model) { 121 | const box = new THREE.Box3().setFromObject(model); 122 | const max = box.max; 123 | const min = box.min; 124 | 125 | return [-min.x, -max.x]; 126 | } 127 | 128 | export { Clip }; 129 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/effect.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; 3 | import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; 4 | import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; 5 | import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; 6 | 7 | function setupComposer(scene, camera, renderer) { 8 | // 创建渲染通道 9 | const renderPass = new RenderPass(scene, camera); 10 | const params = { 11 | threshold: 0.2, 12 | strength: 0.35, 13 | radius: 0, 14 | }; 15 | // 创建辉光通道 16 | const bloomPass = new UnrealBloomPass( 17 | new THREE.Vector2(window.innerWidth, window.innerHeight) 18 | ); 19 | bloomPass.threshold = params.threshold; 20 | bloomPass.strength = params.strength; 21 | bloomPass.radius = params.radius; 22 | // 创建输出通道 23 | const outputPass = new OutputPass(); 24 | // 创建合成器,组合通道 25 | const composer = new EffectComposer(renderer); 26 | composer.addPass(renderPass); 27 | composer.addPass(bloomPass); 28 | composer.addPass(outputPass); 29 | 30 | return composer; 31 | } 32 | 33 | export { setupComposer }; 34 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/index.js: -------------------------------------------------------------------------------- 1 | import { showToast } from "./toast.js"; 2 | import { loadModel, loadTexture } from "./load.js"; 3 | import { getSketchInfos, createSketch } from "./sketch.js"; 4 | import { setupComposer } from "./effect.js"; 5 | import { PickHelper } from "./pick.js"; 6 | import { App } from "./app.js"; 7 | import { Clip } from "./clip.js"; 8 | import * as kit from "./kit.js"; 9 | 10 | export { showToast, loadModel, loadTexture, getSketchInfos, createSketch, setupComposer, PickHelper, App, Clip }; 11 | export { kit }; 12 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/kit.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils"; 3 | 4 | export function throttle(func, wait) { 5 | let runAble = true; 6 | return function () { 7 | if (runAble) { 8 | func.apply(this, arguments); 9 | runAble = false; 10 | setTimeout(() => { 11 | runAble = true; 12 | }, wait); 13 | } 14 | }; 15 | } 16 | 17 | export function debounce(func, wait) { 18 | let timer = null; 19 | return function () { 20 | clearTimeout(timer); 21 | timer = setTimeout(() => { 22 | func.apply(this, arguments); 23 | }, wait); 24 | }; 25 | } 26 | 27 | // 合并模型的所有几何体 28 | export function mergeModelMesh(modelGroup) { 29 | let mergedGeometry = new THREE.BufferGeometry(); 30 | let geometries = []; 31 | 32 | modelGroup.traverse((item) => { 33 | if (item.isMesh) { 34 | const _geometry = item.geometry.clone(); 35 | _geometry.applyMatrix4(item.matrixWorld); 36 | geometries.push(_geometry); 37 | } 38 | }); 39 | 40 | mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries); 41 | 42 | const mergedMesh = new THREE.Mesh( 43 | mergedGeometry, 44 | new THREE.MeshBasicMaterial() 45 | ); 46 | 47 | return mergedMesh; 48 | } 49 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/load.js: -------------------------------------------------------------------------------- 1 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; 2 | import { TextureLoader } from "three/src/loaders/TextureLoader.js"; 3 | 4 | const gltfLoader = new GLTFLoader().setPath("./models/"); 5 | 6 | const loadModel = (path, onProgress) => 7 | new Promise((resolve) => { 8 | gltfLoader.load( 9 | path, 10 | (glb) => { 11 | const model = glb.scene; 12 | // 设置shadow 13 | model.receiveShadow = true; 14 | model.castShadow = true; 15 | model.traverse((child) => { 16 | if (child.isMesh) { 17 | child.castShadow = true; 18 | child.receiveShadow = true; 19 | } 20 | }); 21 | resolve(glb); 22 | }, 23 | onProgress, 24 | (e) => console.warn(path + " failed \n [reason] " + e.message) 25 | ); 26 | }); 27 | 28 | const textureLoader = new TextureLoader().setPath("./textures/"); 29 | 30 | const loadTexture = (path) => new Promise((resolve) => textureLoader.load(path, resolve)); 31 | 32 | export { loadModel, loadTexture }; 33 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/outline.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { mergeModelMesh } from "./kit.js"; 3 | import { MeshBVH } from "three-mesh-bvh"; 4 | 5 | // 用于生成模型切割轮廓线的工具类 6 | export default class OutlineGenerator { 7 | // 将所有临时对象组织在一起 8 | temp = { 9 | intersectPoint: new THREE.Vector3(), // 用于存储交点 10 | vector3_1: new THREE.Vector3(), // 用于存储三重交点的第一个三维向量 11 | vector3_2: new THREE.Vector3(), 12 | vector3_3: new THREE.Vector3(), 13 | line: new THREE.Line3(), 14 | matrix: new THREE.Matrix4(), 15 | plane: new THREE.Plane(), 16 | }; 17 | bvh = null; 18 | constructor(model, options = {}) { 19 | // 生成用于碰撞测试的网格 20 | this.bvh_mesh = mergeModelMesh(model); 21 | this.bvh_mesh.scale.copy(model.scale); 22 | this.bvh_mesh.position.copy(model.position); 23 | this.bvh_mesh.rotation.copy(model.rotation); 24 | 25 | this.bvh_mesh.updateMatrixWorld(); 26 | 27 | this.options = { 28 | maxSegments: options.maxSegments || 100000, 29 | color: options.color || 0x00acc1, 30 | }; 31 | 32 | // 初始化BVH 33 | this.bvh = this.initBVH(this.bvh_mesh); 34 | // 初始化轮廓线对象 35 | this.outlines = this.initOutLines(); 36 | } 37 | 38 | // 根据切割平面 更新 轮廓线对象 39 | updateOutLines(clipPlane) { 40 | // 进行坐标空间转换,将切割平面从世界空间转换到模型的局部空间 41 | // clippingPlane 是在世界空间中定义的 42 | // 模型的几何数据(顶点)是在模型的局部空间中定义的 43 | // 要计算平面与模型的交点,需要在同一个空间中进行计算 44 | 45 | // 1. 获取从世界空间到模型局部空间的转换矩阵 46 | // mode.matrixWorld 获取模型的世界矩阵(包含所有变换信息,缩小放大 旋转 平移) 47 | // .invert() 获取逆矩阵(从世界空间转换到局部空间) 48 | this.temp.matrix.copy(this.bvh_mesh.matrixWorld).invert(); 49 | 50 | // 2. 将切割平面转换到模型的局部空间 51 | // 将获得的逆矩阵作用到plane上 52 | this.temp.plane.copy(clipPlane).applyMatrix4(this.temp.matrix); 53 | 54 | let index = 0; 55 | const posAttr = this.outlines.geometry.attributes.position; 56 | 57 | // 使用BVH检测相交 58 | this.bvh.shapecast({ 59 | // 快速检查包围盒是否与切割平面相交 60 | intersectsBounds: (box) => this.temp.plane.intersectsBox(box), 61 | // 对可能相交的三角形进行详细检测 62 | intersectsTriangle: (tri) => { 63 | // 检查三角形的三条边 64 | const edges = [ 65 | [tri.a, tri.b], 66 | [tri.b, tri.c], 67 | [tri.c, tri.a], 68 | ]; 69 | 70 | let count = 0; 71 | for (const [start, end] of edges) { 72 | this.temp.line.start.copy(start); 73 | this.temp.line.end.copy(end); 74 | if ( 75 | this.temp.plane.intersectLine( 76 | this.temp.line, 77 | this.temp.intersectPoint 78 | ) 79 | ) { 80 | posAttr.setXYZ( 81 | index, 82 | this.temp.intersectPoint.x + 1, 83 | this.temp.intersectPoint.y, 84 | this.temp.intersectPoint.z 85 | ); 86 | index++; 87 | count++; 88 | } 89 | } 90 | 91 | // 处理特殊情况(三个交点) 92 | if (count === 3) { 93 | this.handleTripleIntersection(posAttr, index, count); 94 | } 95 | 96 | // 如果不是两个交点,移除这些交点 97 | if (count !== 2) { 98 | index -= count; 99 | } 100 | }, 101 | }); 102 | 103 | // 更新轮廓线显示 104 | this.outlines.geometry.setDrawRange(0, index); 105 | // 稍微偏移轮廓线位置,避免与模型表面重叠 106 | this.outlines.position.copy(clipPlane.normal).multiplyScalar(-0.00001); 107 | posAttr.needsUpdate = true; 108 | 109 | return this.outlines; 110 | } 111 | 112 | // 处理三重交点的特殊情况 113 | handleTripleIntersection(posAttr, index, count) { 114 | this.temp.vector3_1.fromBufferAttribute(posAttr, index - 3); 115 | this.temp.vector3_2.fromBufferAttribute(posAttr, index - 2); 116 | this.temp.vector3_3.fromBufferAttribute(posAttr, index - 1); 117 | // 如果最后一个点是一个重复的交点 118 | if ( 119 | this.temp.vector3_3.equals(this.temp.vector3_1) || 120 | this.temp.vector3_3.equals(this.temp.vector3_2) 121 | ) { 122 | count--; 123 | index--; 124 | } else if (this.temp.vector3_1.equals(this.temp.vector3_2)) { 125 | posAttr.setXYZ( 126 | index - 2, 127 | // this.temp.vector3_3, 128 | this.temp.vector3_3.x + 1, 129 | this.temp.vector3_3.y, 130 | this.temp.vector3_3.z 131 | ); 132 | count--; 133 | index--; 134 | } 135 | } 136 | 137 | initOutLines() { 138 | const lineGeometry = new THREE.BufferGeometry(); 139 | const linePosAttr = new THREE.BufferAttribute( 140 | new Float32Array(this.options.maxSegments * 3), 141 | 3, 142 | false 143 | ); 144 | linePosAttr.setUsage(THREE.DynamicDrawUsage); 145 | lineGeometry.setAttribute("position", linePosAttr); 146 | 147 | const outlines = new THREE.LineSegments( 148 | lineGeometry, 149 | new THREE.LineBasicMaterial({ color: this.options.color }) 150 | ); 151 | 152 | outlines.scale.copy(this.bvh_mesh.scale); 153 | outlines.position.set(0, 0, 0); 154 | outlines.quaternion.identity(); 155 | 156 | return outlines; 157 | } 158 | 159 | // 生成bvh加速结构 160 | initBVH(mesh) { 161 | if (!mesh.isMesh) return; 162 | 163 | const geometry = mesh.geometry.clone(); 164 | const bvh = new MeshBVH(geometry, { 165 | maxLeafTris: 10, // 增加每个叶节点的最大三角形数 166 | maxDepth: 100, // 增加最大深度限制 167 | strategy: 0, // SAH 策略,可以提供更好的树结构 168 | }); 169 | geometry.boundsTree = bvh; 170 | 171 | return bvh; 172 | } 173 | 174 | dispose() { 175 | this.outlines.geometry.dispose(); 176 | this.outlines.material.dispose(); 177 | this.bvh = null; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/pick.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | class PickHelper { 4 | constructor(canvas, highlightColor = 0x90ee90) { 5 | this.canvas = canvas; 6 | this.raycaster = new THREE.Raycaster(); 7 | this.pickedObject = null; 8 | this.pickedObjectSavedMat = null; 9 | this.pickPosition = { x: 0, y: 0 }; 10 | this.highlightColor = highlightColor; 11 | clearPickPosition.apply(this); 12 | window.addEventListener("mousemove", setPickPosition.bind(this)); 13 | window.addEventListener("mouseout", clearPickPosition.bind(this)); 14 | window.addEventListener("mouseleave", clearPickPosition.bind(this)); 15 | } 16 | pick(scene, camera, time) { 17 | // 恢复上一个被拾取对象的颜色 18 | if (this.pickedObject) { 19 | Object.assign(this.pickedObject.material, this.pickedObjectSavedMat || {}); 20 | this.pickedObject = undefined; 21 | } 22 | 23 | // 发出射线 24 | this.raycaster.setFromCamera(this.pickPosition, camera); 25 | // 获取与射线相交的对象 26 | const intersectedObjects = this.raycaster.intersectObjects(scene.children); 27 | if (intersectedObjects.length) { 28 | // 找到第一个对象,它是离鼠标最近的对象 29 | this.pickedObject = intersectedObjects[0].object; 30 | // 缓存材质 31 | const material = this.pickedObject.material; 32 | this.pickedObjectSavedMat = material.clone(); 33 | highlightTarget(this.pickedObject, this.highlightColor); 34 | } 35 | } 36 | } 37 | 38 | function highlightTarget(target, color) { 39 | const material = target.material; 40 | material.transparent = true; 41 | material.opacity = 0.3; 42 | setMaterialColor(material, color); 43 | } 44 | 45 | function getMaterialColor(material) { 46 | const colorKey = material.hasOwnProperty("emissive") ? "emissive" : "color"; 47 | return material?.[colorKey]?.getHex(); 48 | } 49 | 50 | function setMaterialColor(material, color) { 51 | const colorKey = material.hasOwnProperty("emissive") ? "emissive" : "color"; 52 | material?.[colorKey]?.setHex(color); 53 | } 54 | 55 | function getCanvasRelativePosition(event) { 56 | const rect = this.canvas.getBoundingClientRect(); 57 | return { 58 | x: ((event.clientX - rect.left) * this.canvas.width) / rect.width, 59 | y: ((event.clientY - rect.top) * this.canvas.height) / rect.height, 60 | }; 61 | } 62 | 63 | function setPickPosition(event) { 64 | const pos = getCanvasRelativePosition.call(this, event); 65 | this.pickPosition.x = (pos.x / this.canvas.width) * 2 - 1; 66 | this.pickPosition.y = (pos.y / this.canvas.height) * -2 + 1; // note we flip Y 67 | } 68 | 69 | function clearPickPosition() { 70 | // 对于触屏,不像鼠标总是能有一个位置坐标, 71 | // 如果用户不在触摸屏幕,我们希望停止拾取操作。 72 | // 因此,我们选取一个特别的值,表明什么都没选中 73 | this.pickPosition.x = -100000; 74 | this.pickPosition.y = -100000; 75 | } 76 | 77 | export { PickHelper }; 78 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/sketch.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | function getSketchInfos(meshs) { 4 | const sketchInfos = meshs.map((mesh) => { 5 | if (mesh.isMesh) { 6 | // 获取四元数 7 | const quaternion = new THREE.Quaternion(); 8 | mesh.getWorldQuaternion(quaternion); 9 | // 获取世界坐标 10 | const worldPos = new THREE.Vector3(); 11 | mesh.getWorldPosition(worldPos); 12 | // 获取缩放比例 13 | const worldScale = new THREE.Vector3(); 14 | mesh.getWorldScale(worldScale); 15 | 16 | return { 17 | geometry: mesh.geometry, 18 | quaternion, 19 | worldPos, 20 | worldScale, 21 | }; 22 | } 23 | }); 24 | return sketchInfos; 25 | } 26 | 27 | function createLine(material) { 28 | return function (sketchInfo, angle = 1) { 29 | const { geometry, quaternion, worldPos, worldScale } = sketchInfo; 30 | // 创建边缘几何体,以此创建线段 31 | const edges = new THREE.EdgesGeometry(geometry, angle); 32 | const line = new THREE.LineSegments(edges); 33 | line.material = material; 34 | // 通过四元数获取正确的旋转 35 | line.quaternion.copy(quaternion); 36 | line.position.copy(worldPos); 37 | line.scale.copy(worldScale); 38 | return line; 39 | }; 40 | } 41 | 42 | function createSketch(sketchInfos, color = "#0fb2fb", opacity = 0.8) { 43 | const material = new THREE.LineBasicMaterial({ 44 | color: new THREE.Color(color), 45 | transparent: true, 46 | opacity, 47 | }); 48 | 49 | const createLineWithMaterial = createLine(material); 50 | 51 | const sketch = new THREE.Group(); 52 | sketchInfos.forEach((sketchInfo) => { 53 | // 以模型顶点信息创建线条 54 | const line = createLineWithMaterial(sketchInfo); 55 | sketch.add(line); 56 | }); 57 | return sketch; 58 | } 59 | 60 | export { getSketchInfos, createSketch }; 61 | -------------------------------------------------------------------------------- /demo/lineSketch/utils/toast.js: -------------------------------------------------------------------------------- 1 | function showToast(message, top = 30, duration = 3000) { 2 | // 创建toast元素 3 | const toast = document.createElement("div"); 4 | toast.textContent = message; 5 | toast.style.position = "fixed"; 6 | toast.style.top = top + "px"; 7 | toast.style.left = "50%"; 8 | toast.style.transform = "translateX(-50%)"; 9 | toast.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; 10 | toast.style.color = "white"; 11 | toast.style.padding = "10px 20px"; 12 | toast.style.borderRadius = "3px"; 13 | toast.style.zIndex = "1000"; 14 | toast.style.opacity = "0"; 15 | toast.style.transition = "opacity 0.8s"; 16 | document.body.appendChild(toast); 17 | return new Promise((resolve) => { 18 | setTimeout(() => { 19 | toast.style.opacity = "1"; 20 | }, 10); 21 | setTimeout(() => { 22 | toast.style.opacity = "0"; 23 | resolve(true); 24 | setTimeout(() => { 25 | document.body.removeChild(toast); 26 | }, 800); 27 | }, duration); 28 | }); 29 | } 30 | 31 | export { 32 | showToast, 33 | }; 34 | -------------------------------------------------------------------------------- /demo/minecraft/buglist.md: -------------------------------------------------------------------------------- 1 | ### 1. 加载、引入模型后 无论怎么设置都没有纹理 2 | 3 | 关键信息:```Unknown extension “KHR_materials_pbrSpecularGlossiness“``` 4 | 5 | 错误原因:较新版本的 three.js 不包含 spec/gloss PBR 材料,而此模型需要这些材料 6 | 7 | 解决方法: 8 | 1. **在线转换** 9 | 将模型加载到https://gltf.report/ ,然后将结果导出到新文件 10 | 11 | 2. **离线转换** 12 | ```js 13 | npm install --global @gltf-transform/cli 14 | 15 | gltf-transform metalrough input.glb output.glb 16 | ``` 17 | 3. **运行时转换** 18 | ```js 19 | import { WebIO } from '@gltf-transform/core'; 20 | import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions'; 21 | import { metalRough } from '@gltf-transform/functions'; 22 | 23 | // Load model in glTF Transform. 24 | const io = new WebIO().registerExtensions(KHRONOS_EXTENSIONS); 25 | const document = await io.read('path/to/model.glb'); 26 | 27 | // Convert materials. 28 | await document.transform(metalRough()); 29 | 30 | // Write back to GLB. 31 | const glb = await io.writeBinary(document); 32 | 33 | // Load model in three.js. 34 | const loader = new GLTFLoader(); 35 | loader.parse(glb.buffer, '', (gltf) => { 36 | scene.add(gltf.scene); 37 | // ... 38 | }); 39 | ``` -------------------------------------------------------------------------------- /demo/minecraft/font/unifont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/font/unifont.ttf -------------------------------------------------------------------------------- /demo/minecraft/images/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/apple.png -------------------------------------------------------------------------------- /demo/minecraft/images/btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/btn.png -------------------------------------------------------------------------------- /demo/minecraft/images/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/food.png -------------------------------------------------------------------------------- /demo/minecraft/images/hp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/hp.png -------------------------------------------------------------------------------- /demo/minecraft/images/level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/level.png -------------------------------------------------------------------------------- /demo/minecraft/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/logo.png -------------------------------------------------------------------------------- /demo/minecraft/images/menu_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/images/menu_item.png -------------------------------------------------------------------------------- /demo/minecraft/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --progress-value: 0; 3 | } 4 | .menu { 5 | position: absolute; 6 | display: flex; 7 | flex-direction: column; 8 | height: 50vh; 9 | width: 50vw; 10 | min-width: 350px; 11 | max-width: 640px; 12 | user-select: none; 13 | text-align: center; 14 | left: 50%; 15 | transform: translate(-50%, 50%); 16 | transition: transform 0.6s ease-in-out; 17 | } 18 | .logo { 19 | margin-bottom: 30px; 20 | padding: 5px 0; 21 | background-image: url(./images/logo.png); 22 | min-height: 10vh; 23 | background-size: cover; 24 | background-repeat: no-repeat; 25 | background-position: center; 26 | } 27 | .progress { 28 | position: relative; 29 | border: 3px solid white; 30 | height: 20px; 31 | width: 100%; 32 | } 33 | .progress::before { 34 | content: ""; 35 | position: absolute; 36 | margin: 2px; 37 | left: 0; 38 | width: calc(var(--progress-value) * 1%); 39 | max-width: calc(100% - 4px); 40 | height: 16px; 41 | text-align: left; 42 | background-color: white; 43 | } 44 | .progress-number { 45 | margin: 5px 0; 46 | color: white; 47 | font-size: 25px; 48 | font-weight: 600; 49 | height: 25px; 50 | } 51 | .btn { 52 | opacity: 0; 53 | padding: 20px; 54 | margin: 15px 0; 55 | background-image: url(./images/btn.png); 56 | background-size: contain; 57 | background-repeat: no-repeat; 58 | background-position: center; 59 | font-size: 25px; 60 | color: rgb(244, 244, 244); 61 | letter-spacing: 2px; 62 | transform: translateY(20px); 63 | transition: filter 0.5s ease-in-out, opacity 0.5s ease-in-out, 64 | transform 0.4s ease-in-out; 65 | } 66 | .btn:hover { 67 | filter: brightness(1.3); 68 | cursor: pointer; 69 | transition: filter 0.5s ease-in-out, transform 0.4s ease-in-out; 70 | } 71 | 72 | .game-ui { 73 | position: absolute; 74 | display: none; 75 | flex-direction: column; 76 | justify-content: space-between; 77 | box-sizing: border-box; 78 | height: 100vh; 79 | width: 100vw; 80 | pointer-events: none; 81 | } 82 | .chat { 83 | background: #000000af; 84 | position: absolute; 85 | margin-top: 15vh; 86 | } 87 | .chat-item { 88 | margin: 10px; 89 | color: white; 90 | width: 300px; 91 | letter-spacing: 1px; 92 | } 93 | .point { 94 | position: absolute; 95 | font-size: 30px; 96 | color: #ffffffa4; 97 | top: 50%; 98 | left: 50%; 99 | transform: translate(-50%, -50%); 100 | mix-blend-mode: exclusion; 101 | } 102 | .game-header { 103 | padding: 10px; 104 | display: flex; 105 | justify-content: space-between; 106 | } 107 | .game-footer { 108 | padding: 10px; 109 | display: flex; 110 | justify-content: space-between; 111 | } 112 | .footer_center { 113 | width: 40%; 114 | max-width: 400px; 115 | } 116 | .status { 117 | display: flex; 118 | justify-content: space-between; 119 | min-height: 20px; 120 | } 121 | .hp { 122 | background-image: url(./images/hp.png); 123 | width: 40%; 124 | background-size: contain; 125 | background-repeat: round; 126 | } 127 | .food { 128 | background-image: url(./images/food.png); 129 | width: 40%; 130 | background-size: contain; 131 | background-repeat: round; 132 | } 133 | .goods { 134 | display: flex; 135 | border: 4px solid #000000a4; 136 | min-height: 40px; 137 | background-image: url(/demo/minecraft/images/menu_item.png); 138 | background-size: contain; 139 | background-repeat: round; 140 | } 141 | .level { 142 | margin: 10px 0; 143 | min-height: 15px; 144 | background-image: url(./images/level.png); 145 | background-size: contain; 146 | background-repeat: no-repeat; 147 | } 148 | 149 | .label-item { 150 | display: block; 151 | position: absolute; 152 | top: 0; 153 | left: 0; 154 | padding: 10px; 155 | color: #fff; 156 | font-size: 20px; 157 | border-radius: 5px; 158 | background-color: rgba(0, 0, 0, 0.6); 159 | pointer-events: none; 160 | } -------------------------------------------------------------------------------- /demo/minecraft/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | minecraft demo 10 | 11 | 28 | 29 | 30 | 31 |
32 | 47 |
48 |
49 |

<系统>  WASD 或 方向键移动

50 |

<系统>  SPACE 跳跃

51 |

<系统>  QE左右旋转视角

52 |
53 |
+
54 |
55 | 67 |
68 |
69 | 81 | 82 | 95 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /demo/minecraft/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; 3 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 4 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 5 | import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; 6 | import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; 7 | import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; 8 | import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; 9 | import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; 10 | import { 11 | CSS2DRenderer, 12 | CSS2DObject, 13 | } from "three/addons/renderers/CSS2DRenderer.js"; 14 | 15 | const clock = new THREE.Clock(); 16 | const materials = {}; 17 | const darkenMaterial = new THREE.MeshBasicMaterial({ color: "black" }); 18 | const BLOOM_SCENE = 1; 19 | const bloomLayer = new THREE.Layers(); 20 | bloomLayer.set(BLOOM_SCENE); 21 | 22 | let _innerWidth = window.innerWidth; 23 | let _innerHeight = window.innerHeight; 24 | let _aspect = _innerWidth / _innerHeight; 25 | let renderCamera; 26 | 27 | // 创建 CSS2DRenderer 28 | const labelRenderer = new CSS2DRenderer(); 29 | labelRenderer.setSize(_innerWidth, _innerHeight); 30 | labelRenderer.domElement.style.position = "absolute"; 31 | labelRenderer.domElement.style.top = "0"; 32 | labelRenderer.domElement.style.pointerEvents = "none"; 33 | document.body.appendChild(labelRenderer.domElement); 34 | 35 | const basicWidth = 100; 36 | const basicHeight = 80; 37 | const scene = new THREE.Scene(); 38 | scene.background = new THREE.Color("black"); 39 | 40 | const camera = new THREE.PerspectiveCamera(75, _aspect, 0.1, 200); 41 | const personCamera = new THREE.PerspectiveCamera(70, _aspect, 0.5, 130); 42 | 43 | camera.position.set(-7.6, 9.3, 40.3); 44 | camera.lookAt(5, 2, 0); 45 | 46 | // const cameraHelper = new THREE.CameraHelper(camera); 47 | // const cameraHelper2 = new THREE.CameraHelper(personCamera); 48 | renderCamera = camera; 49 | 50 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 51 | renderer.setSize(_innerWidth, _innerHeight); 52 | renderer.shadowMap.enabled = true; 53 | renderer.setPixelRatio(window.devicePixelRatio); 54 | // renderer.toneMapping = THREE.ReinhardToneMapping; 55 | renderer.outputEncoding = THREE.sRGBEncoding; 56 | 57 | const controls = new OrbitControls(renderCamera, renderer.domElement); 58 | controls.update(); 59 | // const axesHelper = new THREE.AxesHelper(5); 60 | // scene.add(axesHelper); 61 | // scene.add(cameraHelper); 62 | // scene.add(cameraHelper2); 63 | // cameraHelper2.add(axesHelper); 64 | 65 | // 云 66 | const cloudPaths = [ 67 | "cloud.glb", 68 | "cloud2.glb", 69 | "cloud3.glb", 70 | "cloud4.glb", 71 | "cloud5.glb", 72 | "cloud6.glb", 73 | ]; 74 | 75 | function showToast(message, top = 30, duration = 3000) { 76 | // 创建toast元素 77 | const toast = document.createElement("div"); 78 | toast.textContent = message; 79 | toast.style.position = "fixed"; 80 | toast.style.top = top + "px"; 81 | toast.style.left = "50%"; 82 | toast.style.transform = "translateX(-50%)"; 83 | toast.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; 84 | toast.style.color = "white"; 85 | toast.style.padding = "10px 20px"; 86 | toast.style.borderRadius = "3px"; 87 | toast.style.zIndex = "1000"; 88 | toast.style.opacity = "0"; 89 | toast.style.transition = "opacity 0.8s"; 90 | document.body.appendChild(toast); 91 | return new Promise((resolve) => { 92 | setTimeout(() => { 93 | toast.style.opacity = "1"; 94 | }, 10); 95 | setTimeout(() => { 96 | toast.style.opacity = "0"; 97 | resolve(true); 98 | setTimeout(() => { 99 | document.body.removeChild(toast); 100 | }, 800); 101 | }, duration); 102 | }); 103 | } 104 | 105 | function curryShowToast(key) { 106 | const keys = new Set(); 107 | let count = 0; 108 | return function (message) { 109 | if (keys.has(key)) return; 110 | key && keys.add(key); 111 | const top = 50 + count * 50; 112 | ++count; 113 | showToast(message, top, 3000).then(() => { 114 | --count; 115 | key && keys.delete(key); 116 | }); 117 | }; 118 | } 119 | 120 | const showNormalMsg = curryShowToast(); 121 | const showSingleMsg = curryShowToast("out"); 122 | const showOutBorder = () => showSingleMsg("前面的区域以后再来探索吧"); 123 | const showWelcome = () => showNormalMsg("欢迎进入Minecraft"); 124 | const showLockPointer = () => showNormalMsg("已锁定鼠标 ESC退出锁定"); 125 | const showUnLockPointer = () => showNormalMsg("已退出锁定"); 126 | 127 | function addProgress(progress = 0) { 128 | const element = document.getElementById("progress"); 129 | const valueElement = document.getElementById("progress-value"); 130 | return function (increment) { 131 | progress = Math.min(100, progress + increment); 132 | document.documentElement.style.setProperty("--progress-value", progress); 133 | element.setAttribute("value", progress); 134 | valueElement.innerText = `${progress}%`; 135 | if (progress === 100) { 136 | document.getElementById("menu").style.transform = "translate(-50%, 30%)"; 137 | setTimeout(() => { 138 | document.body.appendChild(renderer.domElement); 139 | document.getElementById("loading").style.display = "none"; 140 | [...document.getElementsByClassName("btn")].forEach((btn) => { 141 | btn.style.opacity = 1; 142 | btn.style.transform = "translateY(0)"; 143 | }); 144 | }, 550); 145 | } 146 | return progress; 147 | }; 148 | } 149 | const incrementProgress = addProgress(0); 150 | 151 | const loadModel = (path) => 152 | new Promise((resolve) => { 153 | gltfLoader.load( 154 | path, 155 | (glb) => { 156 | const model = glb.scene; 157 | model.receiveShadow = true; 158 | model.castShadow = true; 159 | model.traverse((child) => { 160 | if (child.isMesh) { 161 | child.castShadow = true; 162 | child.receiveShadow = true; 163 | } 164 | }); 165 | resolve(glb); 166 | }, 167 | () => {}, 168 | (e) => console.warn(path + " failed \n [reason] " + e.message) 169 | ); 170 | }).finally(() => incrementProgress(10)); 171 | 172 | const modelsLoader = { 173 | houseLoader: () => loadModel("house-v1.glb"), 174 | wolfLoader: () => loadModel("wolf.glb"), 175 | foxLoader: () => loadModel("fox.glb"), 176 | benchLoader: () => loadModel("bench.glb"), 177 | customHouseLoader: () => loadModel("custom_house.glb"), 178 | blockLoader: () => loadModel("block.glb"), 179 | villageLoader: () => loadModel("village-v1.glb"), 180 | steveLoader: () => loadModel("steve.glb"), 181 | plantLoader: () => 182 | Promise.all( 183 | ["tree.glb", "grass.glb", "flower.glb"].map((path) => loadModel(path)) 184 | ), 185 | cloudLoader: () => Promise.all(cloudPaths.map((path) => loadModel(path))), 186 | }; 187 | 188 | // 增加 半球光 189 | { 190 | const skyColor = "#a3cee9"; 191 | const groundColor = 0x8e8e8e; 192 | // const skyColor = 0xb1e1ff; 193 | // const groundColor = 0xeeeeee; 194 | const intensity = 5; 195 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity); 196 | scene.add(light); 197 | } 198 | 199 | // 方向光 模拟太阳 200 | { 201 | const color = 0xffffff; 202 | const intensity = 10; 203 | const light = new THREE.DirectionalLight(color, intensity); 204 | light.castShadow = true; 205 | light.shadow.camera.left = basicWidth / -2; 206 | light.shadow.camera.right = basicWidth / 2; 207 | light.shadow.camera.top = basicHeight / -1; 208 | light.shadow.camera.bottom = basicHeight / 1; 209 | light.shadow.camera.far = 90; 210 | light.position.set(0, 55, 30); 211 | // const helper = new THREE.DirectionalLightHelper(light); 212 | // const cameraHelper = new THREE.CameraHelper(light.shadow.camera); 213 | // scene.add(cameraHelper); 214 | scene.add(light); 215 | // scene.add(helper); 216 | } 217 | 218 | const gltfLoader = new GLTFLoader().setPath("./models/"); 219 | 220 | // 地面 221 | const groundGroup = new THREE.Group(); 222 | const firstViewGroup = new THREE.Group(); 223 | const personGroup = new THREE.Group(); 224 | const sunGroup = new THREE.Group(); 225 | 226 | firstViewGroup.add(personGroup); 227 | firstViewGroup.add(sunGroup); 228 | 229 | let steve_model; 230 | let modelControls = {}; 231 | let steve_mixer; 232 | let fox_model; 233 | let fox_mixer; 234 | const curve = new THREE.SplineCurve([ 235 | new THREE.Vector2(-10, 0), 236 | new THREE.Vector2(-5, 5), 237 | new THREE.Vector2(0, 0), 238 | new THREE.Vector2(5, -5), 239 | new THREE.Vector2(10, 0), 240 | new THREE.Vector2(5, 5), 241 | new THREE.Vector2(-5, 5), 242 | new THREE.Vector2(-10, -10), 243 | new THREE.Vector2(-15, -8), 244 | new THREE.Vector2(-10, 0), 245 | ]); 246 | const labelModels = []; 247 | 248 | groundGroup.add(firstViewGroup); 249 | { 250 | // 生成地板 251 | const groundWidth = basicWidth; 252 | const groundHeight = basicHeight; 253 | 254 | const roundTexture = new THREE.TextureLoader().load("./texture/floor2.png"); 255 | roundTexture.wrapS = THREE.RepeatWrapping; 256 | roundTexture.wrapT = THREE.RepeatWrapping; 257 | roundTexture.magFilter = THREE.NearestFilter; 258 | roundTexture.colorSpace = THREE.SRGBColorSpace; 259 | roundTexture.repeat.set(groundWidth / 2, groundHeight / 2); 260 | 261 | const roundGeometry = new THREE.PlaneGeometry(groundWidth, groundHeight); 262 | const roundMaterial = new THREE.MeshPhongMaterial({ 263 | side: THREE.DoubleSide, 264 | map: roundTexture, 265 | }); 266 | const roundMesh = new THREE.Mesh(roundGeometry, roundMaterial); 267 | roundMesh.receiveShadow = true; 268 | roundMesh.rotateX(Math.PI * -0.5); 269 | groundGroup.add(roundMesh); 270 | 271 | // 放入模型 272 | modelsLoader.houseLoader().then((glb) => { 273 | const model = glb.scene; 274 | model.traverse((child) => { 275 | if (child.isMesh) { 276 | child.renderOrder = 1; 277 | child.matrixWorldNeedsUpdate = true; 278 | } 279 | }); 280 | model.position.set(25, 11.605, -10); 281 | model.scale.set(0.8, 0.8, 0.8); 282 | scene.add(model); 283 | }); 284 | 285 | modelsLoader.wolfLoader().then((glb) => { 286 | const model = glb.scene; 287 | model.position.set(0, 0.4, 5); 288 | model.rotateY(Math.PI * 0.5); 289 | model.scale.set(0.05, 0.05, 0.05); 290 | model.name = "wolf"; 291 | 292 | // createLabelByModel(model); 293 | groundGroup.add(model); 294 | }); 295 | 296 | modelsLoader.foxLoader().then((glb) => { 297 | const model = glb.scene; 298 | fox_model = model; 299 | 300 | fox_mixer = new THREE.AnimationMixer(model); 301 | const action = fox_mixer.clipAction(glb.animations[0]); 302 | modelControls.fox_run = () => action.play(); 303 | modelControls.fox_run(); 304 | fox_mixer.clipAction(glb.animations[0]).play(); 305 | 306 | model.name = "fox"; 307 | model.position.set(0, 0.44, 0); 308 | model.rotateY(Math.PI * 1); 309 | model.scale.set(0.4, 0.4, 0.4); 310 | 311 | // const points = curve.getPoints(50); 312 | // const geometry = new THREE.BufferGeometry().setFromPoints(points); 313 | // const material = new THREE.LineBasicMaterial({ color: 0xff0000 }); 314 | // const splineObject = new THREE.Line(geometry, material); 315 | // splineObject.rotation.x = Math.PI * 0.5; 316 | // splineObject.position.y = 0.05; 317 | // scene.add(splineObject); 318 | 319 | createLabelByModel(model); 320 | labelModels.push(model); 321 | 322 | groundGroup.add(model); 323 | }); 324 | 325 | modelsLoader.plantLoader().then((glbs) => { 326 | const tree_model = glbs[0].scene; 327 | const grass_model = glbs[1].scene; 328 | const flower_model = glbs[2].scene; 329 | tree_model.scale.set(8, 8, 8); 330 | grass_model.scale.set(0.7, 0.7, 0.7); 331 | 332 | const tree_models = Array.from({ length: 14 }, () => tree_model.clone()); 333 | const positions = [ 334 | [0, -30], 335 | [10, -10], 336 | [40, -10], 337 | [38, 5], 338 | [25, -30], 339 | [22.5, 7.5], 340 | [20, 22.5], 341 | [35, 20], 342 | [-35, 20], 343 | [-25, -30], 344 | [-25, 10], 345 | [-12, -10], 346 | [-22, 25], 347 | [-38, 3], 348 | ]; 349 | const get_random_number = () => ~~(Math.random() * 20 - 10); 350 | tree_models.forEach((model, index) => { 351 | const position = positions[index]; 352 | model.position.set(position[0], -3.8, position[1]); 353 | groundGroup.add(model); 354 | 355 | const grass_models = Array.from({ length: 5 }, () => grass_model.clone()); 356 | const flower_models = Array.from({ length: 2 }, () => 357 | flower_model.clone() 358 | ); 359 | grass_models.forEach((grass_model) => 360 | grass_model.position.set( 361 | position[0] + get_random_number(), 362 | 0, 363 | position[1] + get_random_number() 364 | ) 365 | ); 366 | grass_models.forEach((model) => groundGroup.add(model)); 367 | flower_models.forEach((flower_model) => 368 | flower_model.position.set( 369 | position[0] + get_random_number(), 370 | 0, 371 | position[1] + get_random_number() 372 | ) 373 | ); 374 | flower_models.forEach((model) => groundGroup.add(model)); 375 | }); 376 | }); 377 | 378 | modelsLoader.benchLoader().then((glb) => { 379 | const model = glb.scene; 380 | model.position.set(0, 0, -5); 381 | model.name = "椅子(不能坐)"; 382 | 383 | const label = createLabelByModel(model); 384 | label.center.set(0, 0); 385 | labelModels.push(model); 386 | 387 | groundGroup.add(model); 388 | }); 389 | 390 | modelsLoader.customHouseLoader().then((glb) => { 391 | const model = glb.scene; 392 | model.position.set(-30, 6.02, -10); 393 | model.scale.set(0.5, 0.505, 0.5); 394 | model.traverse((child) => { 395 | if (child.isMesh) { 396 | child.material.side = THREE.DoubleSide; 397 | child.material.depthWrite = true; 398 | child.material.depthTest = true; 399 | } 400 | }); 401 | groundGroup.add(model); 402 | }); 403 | 404 | modelsLoader.blockLoader().then((glb) => { 405 | const model = glb.scene; 406 | model.position.set(15, 2, 1); 407 | groundGroup.add(model); 408 | }); 409 | 410 | modelsLoader.villageLoader().then((glb) => { 411 | const model = glb.scene; 412 | model.traverse((child) => { 413 | if (child.isMesh) { 414 | child.material.transparent = true; 415 | } 416 | }); 417 | model.position.set(0, 8.01, 22); 418 | model.rotateY(Math.PI); 419 | model.renderOrder = 1; 420 | groundGroup.add(model); 421 | }); 422 | 423 | modelsLoader.steveLoader().then((glb) => { 424 | const model = glb.scene; 425 | 426 | steve_mixer = new THREE.AnimationMixer(model); 427 | const action = steve_mixer.clipAction(glb.animations[0]); 428 | modelControls.walk = () => { 429 | action.paused = false; 430 | action.play(); 431 | }; 432 | modelControls.stop = () => { 433 | action.paused = true; 434 | }; 435 | 436 | steve_model = model; 437 | model.scale.set(0.0016, 0.0016, 0.0016); 438 | model.position.set(0, 0.95, 0); 439 | model.renderOrder = 1; 440 | model.name = "steve"; 441 | model.matrixWorldNeedsUpdate = true; 442 | model.traverse((child) => { 443 | if (child.isMesh) { 444 | child.side = THREE.FrontSide; 445 | } 446 | }); 447 | personGroup.add(model); 448 | 449 | { 450 | const sunRadius = 4; 451 | const sunSegments = 28; 452 | const sunTexture = new THREE.TextureLoader().load("./texture/sun.png"); 453 | const sunGeoMetry = new THREE.CircleGeometry(sunRadius, sunSegments); 454 | // sunTexture.magFilter = THREE.NearestFilter; 455 | 456 | const sunMaterial = new THREE.MeshPhongMaterial({ 457 | side: THREE.DoubleSide, 458 | map: sunTexture, 459 | // transparent: true, 460 | // emissive: 0xffffff, 461 | }); 462 | const sunMesh = new THREE.Mesh(sunGeoMetry, sunMaterial); 463 | sunMesh.name = "sun-material"; 464 | 465 | sunGroup.add(sunMesh); 466 | sunGroup.position.set(0, 39.5, 20); 467 | sunGroup.rotateX(Math.PI * 0.5); 468 | sunMesh.layers.enable(BLOOM_SCENE); 469 | } 470 | 471 | personGroup.add(personCamera); 472 | personCamera.position.setY(1.8); 473 | personCamera.position.setZ(0.5); 474 | personCamera.rotateY(Math.PI); 475 | personCamera.rotation.x = Math.PI; 476 | personCamera.updateMatrixWorld(); 477 | personCamera.name = "personCamera"; 478 | }); 479 | } 480 | 481 | // 天空 482 | const skyGroup = new THREE.Group(); 483 | skyGroup.position.set(0, 40, 0); 484 | 485 | const cloudGroup = new THREE.Group(); 486 | skyGroup.add(cloudGroup); 487 | 488 | // 后期处理 489 | const skyWidth = basicWidth; 490 | const skyHeight = basicHeight; 491 | const params = { 492 | threshold: 0, // 辉光强度 493 | strength: 0.8, // 辉光阈值 494 | radius: 1, // 辉光半径 495 | exposure: 3, 496 | }; 497 | const pixelationShader = { 498 | uniforms: { 499 | tDiffuse: { value: null }, 500 | resolution: { 501 | value: new THREE.Vector2(_innerWidth, _innerHeight), 502 | }, 503 | pixelSize: { value: 10.0 }, // Adjust this value to control pixelation size 504 | }, 505 | vertexShader: ` 506 | varying vec2 vUv; 507 | void main() { 508 | vUv = uv; 509 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 510 | } 511 | `, 512 | fragmentShader: ` 513 | uniform sampler2D tDiffuse; 514 | uniform vec2 resolution; 515 | uniform float pixelSize; 516 | 517 | varying vec2 vUv; 518 | 519 | void main() { 520 | vec2 dxy = pixelSize / resolution; 521 | vec2 coord = dxy * floor(vUv / dxy); 522 | gl_FragColor = texture2D(tDiffuse, coord); 523 | } 524 | `, 525 | }; 526 | 527 | let renderScene = new RenderPass(scene, renderCamera); // 创建一个渲染通道 528 | 529 | // 创建辉光效果 530 | const bloomPass = new UnrealBloomPass( 531 | new THREE.Vector2(_innerWidth, _innerHeight) 532 | ); 533 | bloomPass.threshold = params.threshold; 534 | bloomPass.strength = params.strength; 535 | bloomPass.radius = params.radius; 536 | renderer.toneMappingExposure = Math.pow(params.exposure, 4.0); 537 | // 创建马赛克效果 538 | const pixelationPass = new ShaderPass(pixelationShader); 539 | pixelationPass.needsSwap = true; 540 | 541 | // 辉光合成器 542 | const glowComposer = new EffectComposer(renderer); 543 | glowComposer.renderToScreen = false; 544 | glowComposer.addPass(bloomPass); 545 | glowComposer.addPass(pixelationPass); 546 | 547 | // 创建辉光效果 548 | const mixPass = new ShaderPass( 549 | new THREE.ShaderMaterial({ 550 | uniforms: { 551 | baseTexture: { value: null }, 552 | bloomTexture: { value: glowComposer.renderTarget2.texture }, 553 | }, 554 | vertexShader: document.getElementById("vertexshader").textContent, 555 | fragmentShader: document.getElementById("fragmentshader").textContent, 556 | defines: {}, 557 | }), 558 | "baseTexture" 559 | ); 560 | 561 | const outputPass = new OutputPass(); 562 | 563 | // 场景渲染器 564 | const finalComposer = new EffectComposer(renderer); // 创建效果组合器 565 | finalComposer.addPass(renderScene); 566 | finalComposer.addPass(mixPass); 567 | finalComposer.addPass(outputPass); 568 | 569 | finalComposer.setSize(_innerWidth, _innerHeight); 570 | glowComposer.setSize(_innerWidth, _innerHeight); 571 | 572 | { 573 | { 574 | modelsLoader.cloudLoader().then((glbs) => { 575 | Array(4) 576 | .fill(glbs) 577 | .flat() 578 | .map((glb) => glb.scene) 579 | .forEach((model) => { 580 | model.traverse((child) => { 581 | if (child.isMesh) { 582 | child.castShadow = true; 583 | child.receiveShadow = true; 584 | child.material = new THREE.MeshPhysicalMaterial({ 585 | // color: 'rgb(217,229,253)', 586 | // transparent: true, 587 | // opacity: 0.9, 588 | // depthWrite: false, 589 | // depthTest: true, 590 | metalness: 0.0, //玻璃非金属 591 | roughness: 0.1, //玻璃表面光滑 592 | transmission: 0.45, 593 | }); 594 | } 595 | }); 596 | const _model = model.clone(); 597 | _model.position.set( 598 | Math.random() * skyWidth - skyWidth / 2, 599 | -1.18, 600 | Math.random() * skyHeight - skyHeight / 2 601 | ); 602 | model.position.set( 603 | Math.random() * skyWidth - skyWidth / 2, 604 | -1.18, 605 | Math.random() * skyHeight - skyHeight / 2 606 | ); 607 | _model.rotateY(Math.PI * 0.5); 608 | model.castShadow = true; 609 | // model.receiveShadow = true; 610 | cloudGroup.add(_model); 611 | cloudGroup.add(model); 612 | }); 613 | }); 614 | } 615 | } 616 | 617 | // 天空盒 618 | { 619 | const skyBoxGeoMetry = new THREE.BoxGeometry( 620 | basicWidth, 621 | basicHeight, 622 | basicHeight 623 | ); 624 | // 创建渐变 将Canvas作为纹理 625 | let texture; 626 | let _texture; 627 | { 628 | const canvas = document.createElement("canvas"); 629 | const ctx = canvas.getContext("2d"); 630 | canvas.width = 512; 631 | canvas.height = 512; 632 | const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); 633 | gradient.addColorStop(0.494, "white"); 634 | gradient.addColorStop(0.5, "lightgreen"); 635 | gradient.addColorStop(0.506, "white"); 636 | 637 | ctx.fillStyle = gradient; 638 | ctx.fillRect(0, 0, canvas.width, canvas.height); 639 | 640 | texture = new THREE.Texture(canvas); 641 | // texture.magFilter = THREE.NearestFilter; 642 | // texture.colorSpace = THREE.SRGBColorSpace; 643 | texture.needsUpdate = true; 644 | texture.wrapS = THREE.RepeatWrapping; 645 | texture.wrapT = THREE.RepeatWrapping; 646 | texture.magFilter = THREE.NearestFilter; 647 | texture.colorSpace = THREE.SRGBColorSpace; 648 | _texture = texture.clone(); 649 | _texture.center = new THREE.Vector2(0.5, 0.5); 650 | _texture.rotation = -Math.PI / 2; 651 | _texture.needsUpdate = true; 652 | } 653 | 654 | const skyBoxBorderMaterial = new THREE.MeshBasicMaterial({ 655 | color: "rgb(141,174,252)", 656 | side: THREE.BackSide, 657 | map: texture, 658 | }); 659 | const _skyBoxBorderMaterial = new THREE.MeshBasicMaterial({ 660 | color: "rgb(141,174,252)", 661 | side: THREE.BackSide, 662 | map: _texture, 663 | }); 664 | const skyBoxMaterial = new THREE.MeshBasicMaterial({ 665 | color: "rgb(141,174,252)", 666 | side: THREE.BackSide, 667 | }); 668 | 669 | const skyBoxMesh = new THREE.Mesh(skyBoxGeoMetry, [ 670 | _skyBoxBorderMaterial, 671 | _skyBoxBorderMaterial, 672 | skyBoxBorderMaterial, 673 | skyBoxBorderMaterial, 674 | skyBoxMaterial, 675 | skyBoxMaterial, 676 | ]); 677 | skyBoxMesh.rotateX(Math.PI * -0.5); 678 | scene.add(skyBoxMesh); 679 | } 680 | 681 | // 创建GUI 682 | // const gui = new GUI(); 683 | // const options = { 684 | // renderType: "third", 685 | // }; 686 | // gui 687 | // .add(options, "renderType", ["first", "third"]) 688 | // .name("游戏视角") 689 | // .onChange(changeRolePerspective); 690 | 691 | // 放入模型 692 | scene.add(groundGroup); 693 | scene.add(skyGroup); 694 | 695 | const changeComposerCamera = (camera) => { 696 | if (!camera || !finalComposer || !glowComposer) return; 697 | glowComposer.removePass(renderScene); 698 | finalComposer.removePass(renderScene); 699 | renderScene = new RenderPass(scene, camera); 700 | glowComposer.insertPass(renderScene, 0); 701 | finalComposer.insertPass(renderScene, 0); 702 | }; 703 | function updateLabels() { 704 | if (renderCamera !== personCamera) return; 705 | const camera_position = renderCamera.getWorldPosition(new THREE.Vector3()); 706 | labelModels.forEach((model) => { 707 | model.label.element.style.visibility = "visible"; 708 | model.label.lookAt(camera_position); 709 | }); 710 | labelRenderer.render(scene, renderCamera); 711 | } 712 | function createLabelByModel(model) { 713 | const labelDiv = document.createElement("div"); 714 | labelDiv.className = "label-item"; 715 | labelDiv.textContent = model.name; 716 | const label = new CSS2DObject(labelDiv); 717 | label.element.style.visibility = "hidden"; 718 | label.position.set(0, 2, 0); 719 | model.label = label; 720 | model.add(label); 721 | return label; 722 | } 723 | 724 | function changeRolePerspective(type) { 725 | const cameraMap = { 726 | first: personCamera, 727 | third: camera, 728 | }; 729 | const targetCamera = cameraMap[type]; 730 | if (!targetCamera) return; 731 | renderCamera = targetCamera; 732 | changeComposerCamera(renderCamera); 733 | 734 | controls.enabled = renderCamera === camera; 735 | personGroup.rotation.y = 0; 736 | personGroup.rotation.x = 0; 737 | steve_model.visible = renderCamera === personCamera; 738 | } 739 | // 定义缓动函数 740 | function bezier(t) { 741 | return t > 1 ? 0 : 4 * t * (1 - t); 742 | } 743 | 744 | const jump = (function () { 745 | let isJumping = false; 746 | return () => { 747 | if (isJumping) return; 748 | isJumping = true; 749 | const startTime = Date.now(); 750 | const jumping = () => 751 | requestAnimationFrame(() => { 752 | const t = (Date.now() - startTime) / 800; 753 | const progress = bezier(t); 754 | personGroup.position.y = 2 * progress; 755 | if (t > 1) { 756 | personGroup.position.y = 0; 757 | isJumping = false; 758 | return; 759 | } 760 | requestAnimationFrame(jumping); 761 | }); 762 | jumping(); 763 | }; 764 | })(); 765 | 766 | // 监听鼠标移动事件 控制人物朝向和视角 767 | const max_x_rotation = Math.PI / 2; 768 | const min_x_rotation = Math.PI * 1.3; 769 | function onMouseMove(event) { 770 | if (renderCamera !== personCamera) return; 771 | 772 | const move_x = Math.abs(event.movementX) > 1 ? event.movementX : 0; 773 | const move_y = Math.abs(event.movementY) > 1 ? event.movementY : 0; 774 | 775 | const x_rotate = move_y * 0.0012 * Math.PI; 776 | const y_rotate = -move_x * 0.0006 * Math.PI; 777 | 778 | personGroup.rotation.y += y_rotate; 779 | personCamera.rotation.x = Math.min( 780 | Math.max(personCamera.rotation.x + x_rotate, max_x_rotation), 781 | min_x_rotation 782 | ); 783 | } 784 | 785 | document.addEventListener("keydown", (event) => { 786 | const key = event.key.toLowerCase(); 787 | keyStates[key] = true; 788 | if (key === " ") jump(); 789 | }); 790 | 791 | document.addEventListener("keyup", (event) => { 792 | const key = event.key.toLowerCase(); 793 | keyStates[key] = false; 794 | }); 795 | 796 | // 定义移动速度 797 | const baseMoveSpeed = 0.15; 798 | const baseCloudSpeed = 0.012; 799 | let moveSpeed = baseMoveSpeed; 800 | let cloudSpeed = baseCloudSpeed; 801 | 802 | // 键盘状态 803 | const keyStates = { 804 | w: false, 805 | s: false, 806 | a: false, 807 | d: false, 808 | q: false, 809 | e: false, 810 | arrowup: false, 811 | arrowdown: false, 812 | arrowleft: false, 813 | arrowright: false, 814 | }; 815 | const moveCloud = () => { 816 | const clouds = cloudGroup.children; 817 | clouds.forEach((cloud) => { 818 | cloud.position.z += cloudSpeed; 819 | if (cloud.position.z > skyHeight / 2) { 820 | cloud.position.z = -skyHeight / 2; 821 | cloud.rotateY(Math.PI / 2); 822 | } 823 | }); 824 | }; 825 | const computedMoveDistance = (speed, states, angle) => { 826 | const sin = Math.sin(angle); 827 | const cos = Math.cos(angle); 828 | const front = 829 | (states["w"] || states["arrowup"]) - (states["s"] || states["arrowdown"]); 830 | const right = 831 | (states["d"] || states["arrowright"]) - 832 | (states["a"] || states["arrowleft"]); 833 | const x_distance = speed * (sin * front - cos * right); 834 | const z_distance = speed * (sin * right + cos * front); 835 | return [x_distance, z_distance]; 836 | }; 837 | const maxBorderHeight = basicHeight / 2 - 2; 838 | const maxBorderWidth = basicWidth / 2 - 2; 839 | const validateBorder = () => { 840 | const { x, z } = firstViewGroup.position; 841 | const isOut = 842 | x > maxBorderWidth || 843 | x < -maxBorderWidth || 844 | z > maxBorderHeight || 845 | z < -maxBorderHeight; 846 | if (!isOut) return; 847 | firstViewGroup.position.x = 0; 848 | firstViewGroup.position.z = 0; 849 | showOutBorder(); 850 | }; 851 | 852 | function moveFoxModelByCurve(curve) { 853 | const foxCurve = curve; 854 | return function (time) { 855 | if (!fox_model) return; 856 | 857 | const progress = (time * 0.00003) % 1; 858 | const _progress = (progress + 0.01) % 1; 859 | const [positionX, positionZ] = foxCurve.getPointAt(progress); 860 | const [targetX, targetZ] = foxCurve.getPointAt(_progress); 861 | 862 | fox_model.position.set(positionX, fox_model.position.y, positionZ); 863 | fox_model.lookAt(targetX, fox_model.position.y, targetZ); 864 | fox_model.rotateY(Math.PI / 2); 865 | }; 866 | } 867 | const moveFox = moveFoxModelByCurve(curve); 868 | 869 | function moveFirstViewGroup() { 870 | const y_rotationAngle = personGroup.rotation.y; 871 | const [x_distance, z_distance] = computedMoveDistance( 872 | moveSpeed, 873 | keyStates, 874 | y_rotationAngle 875 | ); 876 | if (x_distance || z_distance) modelControls.walk(); 877 | else return modelControls.stop && modelControls.stop(); 878 | firstViewGroup.position.x += x_distance; 879 | firstViewGroup.position.z += z_distance; 880 | validateBorder(); 881 | } 882 | const rotatePerson = () => { 883 | const y_rotationAngle = 884 | (keyStates["q"] - keyStates["e"]) * 0.003 * 2 * Math.PI; 885 | personGroup.rotation.y += y_rotationAngle; 886 | }; 887 | const calculateSpeed = (fps) => { 888 | moveSpeed = baseMoveSpeed * (60 / fps); 889 | cloudSpeed = baseCloudSpeed * (60 / fps); 890 | }; 891 | 892 | function updateMixer(delta) { 893 | steve_mixer && steve_mixer.update(delta); 894 | fox_mixer && fox_mixer.update(delta); 895 | } 896 | 897 | function composerRender() { 898 | scene.traverse(darkenNonBloomed); 899 | glowComposer.render(); 900 | 901 | scene.traverse(restoreMaterial); 902 | finalComposer.render(); 903 | } 904 | 905 | animate(); 906 | 907 | function animate(lastTime = 0) { 908 | requestAnimationFrame(() => animate(curTime)); 909 | controls.update(); 910 | composerRender(); 911 | // renderer.render(scene, renderCamera); 912 | 913 | const curTime = Date.now(); 914 | const fps = ~~(1000 / (curTime - lastTime)) || 60; 915 | 916 | rotatePerson(); 917 | calculateSpeed(fps); 918 | 919 | moveCloud(); 920 | moveFox(lastTime); 921 | moveFirstViewGroup(); 922 | 923 | updateLabels(); 924 | updateMixer(clock.getDelta()); 925 | } 926 | 927 | window.onresize = () => { 928 | _innerWidth = window.innerWidth; 929 | _innerHeight = window.innerHeight; 930 | camera.aspect = _aspect; 931 | personCamera.aspect = _aspect; 932 | renderer.setSize(_innerWidth, _innerHeight); 933 | camera.updateProjectionMatrix(); 934 | personCamera.updateProjectionMatrix(); 935 | }; 936 | 937 | function darkenNonBloomed(obj) { 938 | if (obj.isMesh && bloomLayer.test(obj.layers) === false) { 939 | materials[obj.uuid] = obj.material; 940 | obj.material = darkenMaterial; 941 | } 942 | } 943 | 944 | function restoreMaterial(obj) { 945 | if (materials[obj.uuid]) { 946 | obj.material = materials[obj.uuid]; 947 | delete materials[obj.uuid]; 948 | } 949 | } 950 | 951 | function lockPointer() { 952 | if ( 953 | !("pointerLockElement" in document) && 954 | !("mozPointerLockElement" in document) && 955 | !("webkitPointerLockElement" in document) 956 | ) { 957 | console.log("Pointer lock unavailable in this browser."); 958 | return; 959 | } 960 | if ("mozPointerLockElement" in document) { 961 | console.log( 962 | "Firefox needs full screen to lock mouse. Use Chrome for the time being." 963 | ); 964 | return; 965 | } 966 | const element = document.body; 967 | element.requestPointerLock = 968 | element.requestPointerLock || 969 | element.mozRequestPointerLock || 970 | element.webkitRequestPointerLock; 971 | 972 | element.requestPointerLock(); 973 | } 974 | 975 | function pointerLockChange() { 976 | if (document.pointerLockElement) { 977 | console.log("pointerLockElement", document.pointerLockElement); 978 | showLockPointer(); 979 | } else { 980 | showUnLockPointer(); 981 | } 982 | } 983 | 984 | document.addEventListener("pointerlockchange", pointerLockChange, false); 985 | document.addEventListener("webkitpointerlockchange", pointerLockChange, false); 986 | document.addEventListener("mozpointerlockchange", pointerLockChange, false); 987 | 988 | function startGame() { 989 | document.getElementById("menu").style.pointerEvents = "none"; 990 | document.getElementById("menu").style.transform = "translate(-50%, -100%)"; 991 | document.body.style.filter = "blur(10px)"; 992 | lockPointer(); 993 | setTimeout(() => { 994 | document.getElementById("menu").style.display = "none"; 995 | document.getElementById("ui").style.display = "flex"; 996 | document.body.style.filter = "blur(0px)"; 997 | changeRolePerspective("first"); 998 | showWelcome(); 999 | document.addEventListener("mousemove", onMouseMove); 1000 | }, 800); 1001 | } 1002 | document.getElementById("start").addEventListener("click", startGame); 1003 | -------------------------------------------------------------------------------- /demo/minecraft/models/bench.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/bench.glb -------------------------------------------------------------------------------- /demo/minecraft/models/block.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/block.glb -------------------------------------------------------------------------------- /demo/minecraft/models/chest.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/chest.glb -------------------------------------------------------------------------------- /demo/minecraft/models/cloud.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/cloud.glb -------------------------------------------------------------------------------- /demo/minecraft/models/cloud2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/cloud2.glb -------------------------------------------------------------------------------- /demo/minecraft/models/cloud3.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/cloud3.glb -------------------------------------------------------------------------------- /demo/minecraft/models/cloud4.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/cloud4.glb -------------------------------------------------------------------------------- /demo/minecraft/models/cloud5.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/cloud5.glb -------------------------------------------------------------------------------- /demo/minecraft/models/cloud6.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/cloud6.glb -------------------------------------------------------------------------------- /demo/minecraft/models/custom_house.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/custom_house.glb -------------------------------------------------------------------------------- /demo/minecraft/models/flower.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/flower.glb -------------------------------------------------------------------------------- /demo/minecraft/models/fox.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/fox.glb -------------------------------------------------------------------------------- /demo/minecraft/models/grass-block.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/grass-block.glb -------------------------------------------------------------------------------- /demo/minecraft/models/grass.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/grass.glb -------------------------------------------------------------------------------- /demo/minecraft/models/house-v1.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/house-v1.glb -------------------------------------------------------------------------------- /demo/minecraft/models/lantern.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/lantern.glb -------------------------------------------------------------------------------- /demo/minecraft/models/static_steve.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/static_steve.glb -------------------------------------------------------------------------------- /demo/minecraft/models/steve.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/steve.glb -------------------------------------------------------------------------------- /demo/minecraft/models/torch.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/torch.glb -------------------------------------------------------------------------------- /demo/minecraft/models/tree.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/tree.glb -------------------------------------------------------------------------------- /demo/minecraft/models/village-v1.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/village-v1.glb -------------------------------------------------------------------------------- /demo/minecraft/models/village.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/village.glb -------------------------------------------------------------------------------- /demo/minecraft/models/village2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/village2.glb -------------------------------------------------------------------------------- /demo/minecraft/models/wolf.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/wolf.glb -------------------------------------------------------------------------------- /demo/minecraft/models/wood_shop.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/models/wood_shop.glb -------------------------------------------------------------------------------- /demo/minecraft/move.drawio: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /demo/minecraft/texture/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/texture/floor.png -------------------------------------------------------------------------------- /demo/minecraft/texture/floor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/texture/floor2.png -------------------------------------------------------------------------------- /demo/minecraft/texture/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/demo/minecraft/texture/sun.png -------------------------------------------------------------------------------- /documents/QQ_1736198772143.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/QQ_1736198772143.png -------------------------------------------------------------------------------- /documents/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-1.png -------------------------------------------------------------------------------- /documents/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-2.png -------------------------------------------------------------------------------- /documents/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-3.png -------------------------------------------------------------------------------- /documents/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-4.png -------------------------------------------------------------------------------- /documents/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-5.png -------------------------------------------------------------------------------- /documents/image-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-6.png -------------------------------------------------------------------------------- /documents/image-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-7.png -------------------------------------------------------------------------------- /documents/image-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-8.png -------------------------------------------------------------------------------- /documents/image-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image-9.png -------------------------------------------------------------------------------- /documents/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/image.png -------------------------------------------------------------------------------- /documents/models/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | import models 7 | 8 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /documents/models/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../../node_modules/three/build/three.module.js"; 2 | import { OrbitControls } from "../../node_modules/three/examples/jsm/controls/OrbitControls.js"; 3 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; 4 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 5 | 6 | const scene = new THREE.Scene(); 7 | scene.background = new THREE.Color("#494141"); 8 | 9 | const camera = new THREE.PerspectiveCamera( 10 | 75, 11 | window.innerWidth / window.innerHeight, 12 | 0.1, 13 | 1000 14 | ); 15 | camera.position.set(5, 5, 5); 16 | camera.lookAt(0, 0, 0); 17 | // camera.position.z = 10; 18 | 19 | const renderer = new THREE.WebGLRenderer(); 20 | renderer.setSize(window.innerWidth, window.innerHeight); 21 | // 创建坐标辅助器 22 | const axesHelper = new THREE.AxesHelper(5); 23 | scene.add(axesHelper); 24 | 25 | // 创建鼠标控制器 26 | let controls = new OrbitControls(camera, renderer.domElement); 27 | // controls.autoRotate = true; 28 | // controls.enableDamping = true; 29 | // 监听控制器,每次拖动后重新渲染画面 30 | controls.addEventListener("change", function () { 31 | renderer.render(scene, camera); //执行渲染操作 32 | }); 33 | 34 | // 创建光源 35 | const color = 0xFFFFFF; 36 | const intensity = 3; 37 | const light = new THREE.DirectionalLight(color, intensity); 38 | light.position.set(-1, 2, 4); 39 | scene.add(light); 40 | 41 | const geometry = new THREE.BoxGeometry(1, 1, 1); 42 | const material = new THREE.MeshPhongMaterial({ color: 0x44aa88 }); 43 | const cube = new THREE.Mesh(geometry, material); 44 | cube.position.set(2.5, 0, 0); 45 | 46 | const loader = new GLTFLoader().setPath("./public/"); 47 | let model; 48 | loader.load( 49 | "scene.gltf", 50 | function (gltf) { 51 | console.log("gltf", gltf); 52 | model = gltf.scene; 53 | model.position.set(-2, 0, 0); 54 | model.scale.set(2, 2, 2); 55 | // model.rotation.set(0, -Math.PI / 4, 0, "XYZ"); 56 | model.add(cube); 57 | gui.add(model.position, "x", -5, 5).step(1).name("模型X轴位置"); 58 | scene.add(model); 59 | }, 60 | undefined, 61 | function (error) { 62 | console.error(error); 63 | } 64 | ); 65 | 66 | // 球体-点材质 67 | const radius = 1; 68 | const widthSegments = 16; 69 | const heightSegments = 8; 70 | const sphere_geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments); 71 | const point_material = new THREE.PointsMaterial({ 72 | color: '#ffd1d1', 73 | size: 0.05, // in world units 74 | }); 75 | const points = new THREE.Points(sphere_geometry, point_material); 76 | points.position.set(0, 0, 1); 77 | scene.add(points); 78 | 79 | document.body.appendChild(renderer.domElement); 80 | 81 | window.onresize = () => { 82 | camera.aspect = window.innerWidth / window.innerHeight; 83 | renderer.setSize(window.innerWidth, window.innerHeight); 84 | camera.updateProjectionMatrix(); 85 | }; 86 | 87 | function animate() { 88 | controls.update(); 89 | if (model) { 90 | // model.position.x += 0.01; 91 | // model.rotation.y += 0.01; 92 | // if (model.position.x > 2) { 93 | // model.position.x = -2; 94 | // } 95 | } 96 | cube.rotation.x += 0.01; 97 | cube.rotation.y += 0.01; 98 | points.rotation.y += 0.01; 99 | renderer.render(scene, camera); 100 | requestAnimationFrame(animate); 101 | } 102 | animate(); 103 | 104 | const eventObj = { 105 | ResetCamera: function () { 106 | controls.reset(); 107 | }, 108 | myNumber: 1, 109 | myString: "lil-gui", 110 | }; 111 | 112 | // 创建GUI 113 | const gui = new GUI(); 114 | // 添加按钮 function类型案例 115 | gui.add(eventObj, "ResetCamera").name("重置"); 116 | 117 | // 控制数字 118 | const number_folder = gui.addFolder("number类型UI"); 119 | number_folder.add(eventObj, "myNumber", 0, 1); 120 | number_folder.add(eventObj, "myNumber", 0, 100, 2); // snap to even numbers 121 | number_folder.add(eventObj, "myNumber", [0, 1, 2]); 122 | number_folder.add(eventObj, "myNumber", { Label1: 0, Label2: 1, Label3: 2 }); 123 | 124 | // 创建控制菜单 125 | const folder = gui.addFolder("cube 位置"); 126 | // 控制物体位置 number类型案例 127 | folder 128 | .add(cube.position, "x", -5, 5) 129 | .step(1) 130 | .name("立方体X轴位置") 131 | .onChange((val) => { 132 | console.log("立方体X轴位置", val); 133 | }); 134 | folder 135 | .add(cube.position, "y") 136 | .min(-5) 137 | .max(5) 138 | .step(1) 139 | .name("立方体Y轴位置") 140 | .onFinishChange((val) => { 141 | console.log("立方体Y轴位置", val); 142 | }); 143 | // 线框模式 boolean类型案例 144 | gui.add(cube.material, "wireframe").name("线框模式"); 145 | // 颜色选择器 color类型案例 146 | const colorFormats = { 147 | cubeColor: "#ffffff", 148 | int: 0xffffff, 149 | object: { r: 1, g: 1, b: 1 }, 150 | array: [1, 1, 1], 151 | }; 152 | 153 | gui.addColor(colorFormats, "cubeColor").onChange((val) => { 154 | cube.material.color.set(val); 155 | }); 156 | 157 | gui.add(eventObj, "myString"); 158 | -------------------------------------------------------------------------------- /documents/models/public/scene.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/models/public/scene.bin -------------------------------------------------------------------------------- /documents/models/public/scene.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 2, 5 | "componentType": 5126, 6 | "count": 65532, 7 | "max": [ 8 | 17.537233352661133, 9 | 14.848954200744629, 10 | 13.505134582519531 11 | ], 12 | "min": [ 13 | 0.8310818672180176, 14 | 1.7654600143432617, 15 | 0.0 16 | ], 17 | "type": "VEC3" 18 | }, 19 | { 20 | "bufferView": 2, 21 | "byteOffset": 786384, 22 | "componentType": 5126, 23 | "count": 65532, 24 | "max": [ 25 | 0.9999961256980896, 26 | 0.9999849200248718, 27 | 0.9958547949790955 28 | ], 29 | "min": [ 30 | -0.999994158744812, 31 | -0.9999861121177673, 32 | -0.999925971031189 33 | ], 34 | "type": "VEC3" 35 | }, 36 | { 37 | "bufferView": 1, 38 | "componentType": 5126, 39 | "count": 65532, 40 | "max": [ 41 | 0.9812660217285156, 42 | 0.9966229796409607 43 | ], 44 | "min": [ 45 | 0.024413999170064926, 46 | 0.02588599920272827 47 | ], 48 | "type": "VEC2" 49 | }, 50 | { 51 | "bufferView": 0, 52 | "componentType": 5125, 53 | "count": 344229, 54 | "type": "SCALAR" 55 | }, 56 | { 57 | "bufferView": 2, 58 | "byteOffset": 1572768, 59 | "componentType": 5126, 60 | "count": 65532, 61 | "max": [ 62 | 16.331989288330078, 63 | 13.51545524597168, 64 | 14.519672393798828 65 | ], 66 | "min": [ 67 | 6.295116424560547, 68 | 1.7617850303649902, 69 | 0.005248069763183594 70 | ], 71 | "type": "VEC3" 72 | }, 73 | { 74 | "bufferView": 2, 75 | "byteOffset": 2359152, 76 | "componentType": 5126, 77 | "count": 65532, 78 | "max": [ 79 | 0.9999998211860657, 80 | 0.9998810887336731, 81 | 0.9999542236328125 82 | ], 83 | "min": [ 84 | -0.9996042847633362, 85 | -0.9999746084213257, 86 | -0.9994091987609863 87 | ], 88 | "type": "VEC3" 89 | }, 90 | { 91 | "bufferView": 1, 92 | "byteOffset": 524256, 93 | "componentType": 5126, 94 | "count": 65532, 95 | "max": [ 96 | 0.9396479725837708, 97 | 0.993058979511261 98 | ], 99 | "min": [ 100 | 0.1527950018644333, 101 | 0.004298000130802393 102 | ], 103 | "type": "VEC2" 104 | }, 105 | { 106 | "bufferView": 0, 107 | "byteOffset": 1376916, 108 | "componentType": 5125, 109 | "count": 323727, 110 | "type": "SCALAR" 111 | }, 112 | { 113 | "bufferView": 2, 114 | "byteOffset": 3145536, 115 | "componentType": 5126, 116 | "count": 65532, 117 | "max": [ 118 | 13.811230659484863, 119 | 13.689750671386719, 120 | 13.595035552978516 121 | ], 122 | "min": [ 123 | 1.017507553100586, 124 | 1.847872257232666, 125 | 0.13642644882202148 126 | ], 127 | "type": "VEC3" 128 | }, 129 | { 130 | "bufferView": 2, 131 | "byteOffset": 3931920, 132 | "componentType": 5126, 133 | "count": 65532, 134 | "max": [ 135 | 0.9999947547912598, 136 | 0.9999624490737915, 137 | 0.9997671842575073 138 | ], 139 | "min": [ 140 | -0.9996042847633362, 141 | -0.9999947547912598, 142 | -0.9999315738677979 143 | ], 144 | "type": "VEC3" 145 | }, 146 | { 147 | "bufferView": 1, 148 | "byteOffset": 1048512, 149 | "componentType": 5126, 150 | "count": 65532, 151 | "max": [ 152 | 0.9109060168266296, 153 | 0.985368013381958 154 | ], 155 | "min": [ 156 | 0.0037320000119507313, 157 | 0.003871999913826585 158 | ], 159 | "type": "VEC2" 160 | }, 161 | { 162 | "bufferView": 0, 163 | "byteOffset": 2671824, 164 | "componentType": 5125, 165 | "count": 330411, 166 | "type": "SCALAR" 167 | }, 168 | { 169 | "bufferView": 2, 170 | "byteOffset": 4718304, 171 | "componentType": 5126, 172 | "count": 65532, 173 | "max": [ 174 | 12.865618705749512, 175 | 13.731435775756836, 176 | 14.030960083007813 177 | ], 178 | "min": [ 179 | 2.0596909523010254, 180 | 0.3448653221130371, 181 | 0.34947919845581055 182 | ], 183 | "type": "VEC3" 184 | }, 185 | { 186 | "bufferView": 2, 187 | "byteOffset": 5504688, 188 | "componentType": 5126, 189 | "count": 65532, 190 | "max": [ 191 | 0.9999410510063171, 192 | 0.9999978542327881, 193 | 0.9999759793281555 194 | ], 195 | "min": [ 196 | -0.9934661388397217, 197 | -0.9999570846557617, 198 | -0.9999822378158569 199 | ], 200 | "type": "VEC3" 201 | }, 202 | { 203 | "bufferView": 1, 204 | "byteOffset": 1572768, 205 | "componentType": 5126, 206 | "count": 65532, 207 | "max": [ 208 | 0.9504899978637695, 209 | 0.9994400143623352 210 | ], 211 | "min": [ 212 | 0.017627999186515808, 213 | 0.0037150001153349876 214 | ], 215 | "type": "VEC2" 216 | }, 217 | { 218 | "bufferView": 0, 219 | "byteOffset": 3993468, 220 | "componentType": 5125, 221 | "count": 310602, 222 | "type": "SCALAR" 223 | }, 224 | { 225 | "bufferView": 2, 226 | "byteOffset": 6291072, 227 | "componentType": 5126, 228 | "count": 65532, 229 | "max": [ 230 | 11.551712036132813, 231 | 13.83425235748291, 232 | 20.844572067260742 233 | ], 234 | "min": [ 235 | 0.3996849060058594, 236 | 0.0, 237 | 0.7858371734619141 238 | ], 239 | "type": "VEC3" 240 | }, 241 | { 242 | "bufferView": 2, 243 | "byteOffset": 7077456, 244 | "componentType": 5126, 245 | "count": 65532, 246 | "max": [ 247 | 0.999997615814209, 248 | 0.9999992847442627, 249 | 0.9998701810836792 250 | ], 251 | "min": [ 252 | -0.9997794032096863, 253 | -0.9999974966049194, 254 | -0.9999135732650757 255 | ], 256 | "type": "VEC3" 257 | }, 258 | { 259 | "bufferView": 1, 260 | "byteOffset": 2097024, 261 | "componentType": 5126, 262 | "count": 65532, 263 | "max": [ 264 | 0.9815809726715088, 265 | 0.9994300007820129 266 | ], 267 | "min": [ 268 | 0.04261000081896782, 269 | 0.005119999870657921 270 | ], 271 | "type": "VEC2" 272 | }, 273 | { 274 | "bufferView": 0, 275 | "byteOffset": 5235876, 276 | "componentType": 5125, 277 | "count": 323955, 278 | "type": "SCALAR" 279 | }, 280 | { 281 | "bufferView": 2, 282 | "byteOffset": 7863840, 283 | "componentType": 5126, 284 | "count": 65532, 285 | "max": [ 286 | 10.967477798461914, 287 | 13.761253356933594, 288 | 20.83525848388672 289 | ], 290 | "min": [ 291 | 3.769012928009033, 292 | 0.009854316711425781, 293 | 0.8088741302490234 294 | ], 295 | "type": "VEC3" 296 | }, 297 | { 298 | "bufferView": 2, 299 | "byteOffset": 8650224, 300 | "componentType": 5126, 301 | "count": 65532, 302 | "max": [ 303 | 0.9999975562095642, 304 | 0.9999781250953674, 305 | 0.9999598860740662 306 | ], 307 | "min": [ 308 | -0.9999998807907104, 309 | -0.9999964833259583, 310 | -0.9998615980148315 311 | ], 312 | "type": "VEC3" 313 | }, 314 | { 315 | "bufferView": 1, 316 | "byteOffset": 2621280, 317 | "componentType": 5126, 318 | "count": 65532, 319 | "max": [ 320 | 0.990460991859436, 321 | 0.9997559785842896 322 | ], 323 | "min": [ 324 | 0.00855099968612194, 325 | 0.02482300065457821 326 | ], 327 | "type": "VEC2" 328 | }, 329 | { 330 | "bufferView": 0, 331 | "byteOffset": 6531696, 332 | "componentType": 5125, 333 | "count": 308319, 334 | "type": "SCALAR" 335 | }, 336 | { 337 | "bufferView": 2, 338 | "byteOffset": 9436608, 339 | "componentType": 5126, 340 | "count": 65532, 341 | "max": [ 342 | 9.37242317199707, 343 | 14.565879821777344, 344 | 20.808900833129883 345 | ], 346 | "min": [ 347 | 2.0886998176574707, 348 | 3.0222530364990234, 349 | 1.53098726272583 350 | ], 351 | "type": "VEC3" 352 | }, 353 | { 354 | "bufferView": 2, 355 | "byteOffset": 10222992, 356 | "componentType": 5126, 357 | "count": 65532, 358 | "max": [ 359 | 0.9928763508796692, 360 | 0.9999921917915344, 361 | 0.9999826550483704 362 | ], 363 | "min": [ 364 | -0.9999755024909973, 365 | -0.9999446868896484, 366 | -0.9975959658622742 367 | ], 368 | "type": "VEC3" 369 | }, 370 | { 371 | "bufferView": 1, 372 | "byteOffset": 3145536, 373 | "componentType": 5126, 374 | "count": 65532, 375 | "max": [ 376 | 0.9817370176315308, 377 | 0.9967880249023438 378 | ], 379 | "min": [ 380 | 0.008697999641299248, 381 | 0.0035000001080334187 382 | ], 383 | "type": "VEC2" 384 | }, 385 | { 386 | "bufferView": 0, 387 | "byteOffset": 7764972, 388 | "componentType": 5125, 389 | "count": 336414, 390 | "type": "SCALAR" 391 | }, 392 | { 393 | "bufferView": 2, 394 | "byteOffset": 11009376, 395 | "componentType": 5126, 396 | "count": 65532, 397 | "max": [ 398 | 7.209283828735352, 399 | 13.76820182800293, 400 | 20.37687873840332 401 | ], 402 | "min": [ 403 | 0.0, 404 | 3.765505075454712, 405 | 1.5866031646728516 406 | ], 407 | "type": "VEC3" 408 | }, 409 | { 410 | "bufferView": 2, 411 | "byteOffset": 11795760, 412 | "componentType": 5126, 413 | "count": 65532, 414 | "max": [ 415 | 0.999839186668396, 416 | 0.999301016330719, 417 | 0.9998390674591064 418 | ], 419 | "min": [ 420 | -0.999999463558197, 421 | -0.9999988675117493, 422 | -0.9999473690986633 423 | ], 424 | "type": "VEC3" 425 | }, 426 | { 427 | "bufferView": 1, 428 | "byteOffset": 3669792, 429 | "componentType": 5126, 430 | "count": 65532, 431 | "max": [ 432 | 0.9887030124664307, 433 | 0.9994149804115295 434 | ], 435 | "min": [ 436 | 0.005880000069737434, 437 | 0.004811999853700399 438 | ], 439 | "type": "VEC2" 440 | }, 441 | { 442 | "bufferView": 0, 443 | "byteOffset": 9110628, 444 | "componentType": 5125, 445 | "count": 312768, 446 | "type": "SCALAR" 447 | }, 448 | { 449 | "bufferView": 2, 450 | "byteOffset": 12582144, 451 | "componentType": 5126, 452 | "count": 65532, 453 | "max": [ 454 | 17.643421173095703, 455 | 18.017213821411133, 456 | 20.767601013183594 457 | ], 458 | "min": [ 459 | 0.01996898651123047, 460 | 0.184967041015625, 461 | 0.15523815155029297 462 | ], 463 | "type": "VEC3" 464 | }, 465 | { 466 | "bufferView": 2, 467 | "byteOffset": 13368528, 468 | "componentType": 5126, 469 | "count": 65532, 470 | "max": [ 471 | 0.9999986290931702, 472 | 0.9999752044677734, 473 | 0.9998327493667603 474 | ], 475 | "min": [ 476 | -0.999999463558197, 477 | -0.9999446868896484, 478 | -0.9999423027038574 479 | ], 480 | "type": "VEC3" 481 | }, 482 | { 483 | "bufferView": 1, 484 | "byteOffset": 4194048, 485 | "componentType": 5126, 486 | "count": 65532, 487 | "max": [ 488 | 0.9965699911117554, 489 | 1.0002460479736328 490 | ], 491 | "min": [ 492 | 0.000514000013936311, 493 | 0.0018220000201836228 494 | ], 495 | "type": "VEC2" 496 | }, 497 | { 498 | "bufferView": 0, 499 | "byteOffset": 10361700, 500 | "componentType": 5125, 501 | "count": 266280, 502 | "type": "SCALAR" 503 | }, 504 | { 505 | "bufferView": 2, 506 | "byteOffset": 14154912, 507 | "componentType": 5126, 508 | "count": 38020, 509 | "max": [ 510 | 10.39173412322998, 511 | 14.847251892089844, 512 | 20.862638473510742 513 | ], 514 | "min": [ 515 | 3.4757466316223145, 516 | 0.06380224227905273, 517 | 1.084066390991211 518 | ], 519 | "type": "VEC3" 520 | }, 521 | { 522 | "bufferView": 2, 523 | "byteOffset": 14611152, 524 | "componentType": 5126, 525 | "count": 38020, 526 | "max": [ 527 | 0.999515950679779, 528 | 0.9999921917915344, 529 | 0.9999598860740662 530 | ], 531 | "min": [ 532 | -0.9999998807907104, 533 | -0.9999567270278931, 534 | -0.9990484118461609 535 | ], 536 | "type": "VEC3" 537 | }, 538 | { 539 | "bufferView": 1, 540 | "byteOffset": 4718304, 541 | "componentType": 5126, 542 | "count": 38020, 543 | "max": [ 544 | 0.9807000160217285, 545 | 0.9944660067558289 546 | ], 547 | "min": [ 548 | 0.009069999679923058, 549 | 0.06397099792957306 550 | ], 551 | "type": "VEC2" 552 | }, 553 | { 554 | "bufferView": 0, 555 | "byteOffset": 11426820, 556 | "componentType": 5125, 557 | "count": 143292, 558 | "type": "SCALAR" 559 | } 560 | ], 561 | "asset": { 562 | "extras": { 563 | "author": "Benjamin Bardou (https://sketchfab.com/meryon)", 564 | "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)", 565 | "source": "https://sketchfab.com/3d-models/cesar-louvre-museum-30f43907f7814582b5dacbbadbabbe6d", 566 | "title": "César - Louvre Museum" 567 | }, 568 | "generator": "Sketchfab-12.67.0", 569 | "version": "2.0" 570 | }, 571 | "bufferViews": [ 572 | { 573 | "buffer": 0, 574 | "byteLength": 11999988, 575 | "name": "floatBufferViews", 576 | "target": 34963 577 | }, 578 | { 579 | "buffer": 0, 580 | "byteLength": 5022464, 581 | "byteOffset": 11999988, 582 | "byteStride": 8, 583 | "name": "floatBufferViews", 584 | "target": 34962 585 | }, 586 | { 587 | "buffer": 0, 588 | "byteLength": 15067392, 589 | "byteOffset": 17022452, 590 | "byteStride": 12, 591 | "name": "floatBufferViews", 592 | "target": 34962 593 | } 594 | ], 595 | "buffers": [ 596 | { 597 | "byteLength": 32089844, 598 | "uri": "scene.bin" 599 | } 600 | ], 601 | "images": [ 602 | { 603 | "uri": "textures/lambert5SG_emissive.jpeg" 604 | } 605 | ], 606 | "materials": [ 607 | { 608 | "doubleSided": true, 609 | "emissiveFactor": [ 610 | 1.0, 611 | 1.0, 612 | 1.0 613 | ], 614 | "emissiveTexture": { 615 | "index": 0 616 | }, 617 | "name": "lambert5SG", 618 | "pbrMetallicRoughness": { 619 | "baseColorFactor": [ 620 | 0.0, 621 | 0.0, 622 | 0.0, 623 | 1.0 624 | ], 625 | "metallicFactor": 0.0 626 | } 627 | } 628 | ], 629 | "meshes": [ 630 | { 631 | "name": "Object_0", 632 | "primitives": [ 633 | { 634 | "attributes": { 635 | "NORMAL": 1, 636 | "POSITION": 0, 637 | "TEXCOORD_0": 2 638 | }, 639 | "indices": 3, 640 | "material": 0, 641 | "mode": 4 642 | } 643 | ] 644 | }, 645 | { 646 | "name": "Object_1", 647 | "primitives": [ 648 | { 649 | "attributes": { 650 | "NORMAL": 5, 651 | "POSITION": 4, 652 | "TEXCOORD_0": 6 653 | }, 654 | "indices": 7, 655 | "material": 0, 656 | "mode": 4 657 | } 658 | ] 659 | }, 660 | { 661 | "name": "Object_2", 662 | "primitives": [ 663 | { 664 | "attributes": { 665 | "NORMAL": 9, 666 | "POSITION": 8, 667 | "TEXCOORD_0": 10 668 | }, 669 | "indices": 11, 670 | "material": 0, 671 | "mode": 4 672 | } 673 | ] 674 | }, 675 | { 676 | "name": "Object_3", 677 | "primitives": [ 678 | { 679 | "attributes": { 680 | "NORMAL": 13, 681 | "POSITION": 12, 682 | "TEXCOORD_0": 14 683 | }, 684 | "indices": 15, 685 | "material": 0, 686 | "mode": 4 687 | } 688 | ] 689 | }, 690 | { 691 | "name": "Object_4", 692 | "primitives": [ 693 | { 694 | "attributes": { 695 | "NORMAL": 17, 696 | "POSITION": 16, 697 | "TEXCOORD_0": 18 698 | }, 699 | "indices": 19, 700 | "material": 0, 701 | "mode": 4 702 | } 703 | ] 704 | }, 705 | { 706 | "name": "Object_5", 707 | "primitives": [ 708 | { 709 | "attributes": { 710 | "NORMAL": 21, 711 | "POSITION": 20, 712 | "TEXCOORD_0": 22 713 | }, 714 | "indices": 23, 715 | "material": 0, 716 | "mode": 4 717 | } 718 | ] 719 | }, 720 | { 721 | "name": "Object_6", 722 | "primitives": [ 723 | { 724 | "attributes": { 725 | "NORMAL": 25, 726 | "POSITION": 24, 727 | "TEXCOORD_0": 26 728 | }, 729 | "indices": 27, 730 | "material": 0, 731 | "mode": 4 732 | } 733 | ] 734 | }, 735 | { 736 | "name": "Object_7", 737 | "primitives": [ 738 | { 739 | "attributes": { 740 | "NORMAL": 29, 741 | "POSITION": 28, 742 | "TEXCOORD_0": 30 743 | }, 744 | "indices": 31, 745 | "material": 0, 746 | "mode": 4 747 | } 748 | ] 749 | }, 750 | { 751 | "name": "Object_8", 752 | "primitives": [ 753 | { 754 | "attributes": { 755 | "NORMAL": 33, 756 | "POSITION": 32, 757 | "TEXCOORD_0": 34 758 | }, 759 | "indices": 35, 760 | "material": 0, 761 | "mode": 4 762 | } 763 | ] 764 | }, 765 | { 766 | "name": "Object_9", 767 | "primitives": [ 768 | { 769 | "attributes": { 770 | "NORMAL": 37, 771 | "POSITION": 36, 772 | "TEXCOORD_0": 38 773 | }, 774 | "indices": 39, 775 | "material": 0, 776 | "mode": 4 777 | } 778 | ] 779 | } 780 | ], 781 | "nodes": [ 782 | { 783 | "children": [ 784 | 1 785 | ], 786 | "matrix": [ 787 | 0.04591473564505577, 788 | 0.0, 789 | 0.0, 790 | 0.0, 791 | 0.0, 792 | 1.0195119336543661e-17, 793 | -0.04591473564505577, 794 | 0.0, 795 | 0.0, 796 | 0.04591473564505577, 797 | 1.0195119336543661e-17, 798 | 0.0, 799 | 0.0, 800 | 0.0, 801 | 0.0, 802 | 1.0 803 | ], 804 | "name": "Sketchfab_model" 805 | }, 806 | { 807 | "children": [ 808 | 2 809 | ], 810 | "matrix": [ 811 | 1.0, 812 | 0.0, 813 | 0.0, 814 | 0.0, 815 | 0.0, 816 | 1.0, 817 | 0.0, 818 | 0.0, 819 | 0.0, 820 | 0.0, 821 | 1.0, 822 | 0.0, 823 | -8.453089714050293, 824 | -6.975077152252197, 825 | -5.846226215362549, 826 | 1.0 827 | ], 828 | "name": "CESAR.obj.cleaner.gles" 829 | }, 830 | { 831 | "children": [ 832 | 3, 833 | 4, 834 | 5, 835 | 6, 836 | 7, 837 | 8, 838 | 9, 839 | 10, 840 | 11, 841 | 12 842 | ], 843 | "name": "Object_2" 844 | }, 845 | { 846 | "mesh": 0, 847 | "name": "Object_3" 848 | }, 849 | { 850 | "mesh": 1, 851 | "name": "Object_4" 852 | }, 853 | { 854 | "mesh": 2, 855 | "name": "Object_5" 856 | }, 857 | { 858 | "mesh": 3, 859 | "name": "Object_6" 860 | }, 861 | { 862 | "mesh": 4, 863 | "name": "Object_7" 864 | }, 865 | { 866 | "mesh": 5, 867 | "name": "Object_8" 868 | }, 869 | { 870 | "mesh": 6, 871 | "name": "Object_9" 872 | }, 873 | { 874 | "mesh": 7, 875 | "name": "Object_10" 876 | }, 877 | { 878 | "mesh": 8, 879 | "name": "Object_11" 880 | }, 881 | { 882 | "mesh": 9, 883 | "name": "Object_12" 884 | } 885 | ], 886 | "samplers": [ 887 | { 888 | "magFilter": 9729, 889 | "minFilter": 9987, 890 | "wrapS": 10497, 891 | "wrapT": 10497 892 | } 893 | ], 894 | "scene": 0, 895 | "scenes": [ 896 | { 897 | "name": "Sketchfab_Scene", 898 | "nodes": [ 899 | 0 900 | ] 901 | } 902 | ], 903 | "textures": [ 904 | { 905 | "sampler": 0, 906 | "source": 0 907 | } 908 | ] 909 | } 910 | -------------------------------------------------------------------------------- /documents/models/public/textures/lambert5SG_emissive.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/models/public/textures/lambert5SG_emissive.jpeg -------------------------------------------------------------------------------- /documents/start/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Creating a scene 7 | 8 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documents/start/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../../node_modules/three/build/three.module.js"; 2 | 3 | /** 4 | * step1: 创建场景、相机、渲染器 并将渲染器添加到页面 5 | */ 6 | const scene = new THREE.Scene(); 7 | const camera = new THREE.PerspectiveCamera( 8 | 75, 9 | window.innerWidth / window.innerHeight, 10 | 0.1, 11 | 1000 12 | ); 13 | const renderer = new THREE.WebGLRenderer(); 14 | renderer.setSize(window.innerWidth, window.innerHeight); 15 | 16 | document.body.appendChild(renderer.domElement); 17 | 18 | /** 19 | * step2: 创建几何体、材质、网格 将网格添加到场景 20 | */ 21 | const geometry = new THREE.BoxGeometry(1, 1, 1); 22 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 23 | const cube = new THREE.Mesh(geometry, material); 24 | scene.add(cube); 25 | camera.position.z = 5; 26 | 27 | function animation() { 28 | requestAnimationFrame(animation); 29 | cube.rotation.x += 0.01; 30 | cube.rotation.y += 0.01; 31 | renderer.render(scene, camera); 32 | } 33 | animation(); 34 | -------------------------------------------------------------------------------- /documents/start/创建场景.md: -------------------------------------------------------------------------------- 1 | # threejs 学习笔记 2 | 3 | 官方文档 https://threejs.org/docs/index.html#manual/zh/introduction/ 4 | 5 | ## 安装 6 | 7 | ``` 8 | three.js 9 | npm install --save three 10 | 11 | # vite 12 | npm install --save-dev vite 13 | ``` 14 | 15 | ## 创建 16 | 17 | ### 初始化 18 | 19 | ```js 20 | import * as THREE from "three"; 21 | 22 | const scene = new THREE.Scene(); 23 | const camera = new THREE.PerspectiveCamera( 24 | 75, 25 | window.innerWidth / window.innerHeight, 26 | 0.1, 27 | 1000 28 | ); 29 | 30 | const renderer = new THREE.WebGLRenderer(); 31 | renderer.setSize(window.innerWidth, window.innerHeight); 32 | document.body.appendChild(renderer.domElement); 33 | ``` 34 | 35 | 使用 threejs,我们需要以下几个对象:场景(**scene**)、相机(**camera**)和渲染器(**renderer**) 36 | 37 | `new THREE.Scene()`创建用于存放物体和渲染的场景 38 | 39 | `new THREE.PerspectiveCamera(fov, ratio, near, far)`(透视摄像机)创建了模拟视点的相机 40 | 41 | 接收参数分别为: 42 | 43 | 1. **fov**:视野角度,能看到的场景范围 44 | 2. **aspect ratio**:长宽比,比如在宽屏电视上播放老电影 45 | 3. **near & far**:近/远截面,超出这个范围的物体不会被渲染 46 | 47 | 48 | 49 | `const renderer = new THREE.WebGLRenderer();` 50 | `renderer.setSize( window.innerWidth, window.innerHeight );`创建渲染器,设置渲染区域的宽高 51 | 52 | `document.body.appendChild( renderer.domElement )`将渲染器添加到文档中 53 | 54 | ### 添加物体 55 | 56 | ```js 57 | const geometry = new THREE.BoxGeometry(1, 1, 1); 58 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 59 | const cube = new THREE.Mesh(geometry, material); 60 | scene.add(cube); 61 | 62 | camera.position.z = 5; 63 | ``` 64 | 65 | 流程为创建一个物体对象,创建一个材质,将它们放入一个网格中,将网格放入场景,再将移动摄像机位置避免重叠 66 | 67 | `new THREE.BoxGeometry( 1, 1, 1 )`创建一个立方体对象,参数包含了一个立方体中所有的顶点(**vertices**)和面(**faces**) 68 | 69 | `new THREE.MeshBasicMaterial( { color: 0x00ff00 } )`创建一种材质,指定颜色 供物体使用 70 | 71 | `new THREE.Mesh( geometry, material )`网格包含一个几何体以及作用在此几何体上的材质 72 | 73 | > 在 OpenGL 中,网格通常由顶点(Vertices)和面(Faces)组成。顶点表示网格的各个顶点位置,而面则定义了连接这些顶点的三角形或四边形。 74 | 75 | ### 渲染场景 76 | 77 | ```js 78 | function animate() { 79 | requestAnimationFrame(animate); 80 | cube.rotation.x += 0.01; 81 | cube.rotation.y += 0.01; 82 | renderer.render(scene, camera); 83 | } 84 | animate(); 85 | ``` 86 | -------------------------------------------------------------------------------- /documents/光源.md: -------------------------------------------------------------------------------- 1 | 如何在 three.js 中使用各种不同类型的光照。 2 | ![alt text](QQ_1736198772143.png) 3 | 每添加一个光源到场景中,都会降低 three.js 渲染场景的速度,所以应该尽量使用最少的资源来实现想要的效果 4 | 5 | Light https://threejs.org/docs/index.html#api/zh/lights/Light 6 | 7 | 光源的基类 - 所有其他的光类型都继承了该类描述的属性和方法。 8 | 9 | 1. 环境光(AmbientLight) 10 | 11 | 没有立体感,只是简单地将材质的颜色与光照颜色进行叠加 12 | 13 | ```js 14 | const color = 0xFFFFFF; 15 | const intensity = 1; 16 | const light = new THREE.AmbientLight(color, intensity); 17 | scene.add(light); 18 | ``` 19 | 20 | 2. 半球光(HemisphereLight) 21 | 22 | 从天空到地面两个颜色之间的渐变,与物体材质的颜色作叠加后得到最终的颜色效果 23 | 24 | 一个点受到的光照颜色是由所在平面的朝向(法向量)决定的 —— 面向正上方就受到天空的光照颜色,面向正下方就受到地面的光照颜色,其他角度则是两个颜色渐变区间的颜色。 25 | 26 | 没有太大立体感,适合与其他光照结合使用 27 | 28 | ```js 29 | const skyColor = 0xB1E1FF; // light blue 30 | const groundColor = 0xB97A20; // brownish orange 31 | const intensity = 1; 32 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity); 33 | scene.add(light); 34 | ``` 35 | 36 | 3. 方向光(DirectionalLight) 37 | 38 | 常用来表现太阳光照的效果,光的方向是从它的位置照向目标点的位置。 39 | 40 | ```js 41 | const color = 0xFFFFFF; 42 | const intensity = 1; 43 | const light = new THREE.DirectionalLight(color, intensity); 44 | light.position.set(0, 10, 0); 45 | light.target.position.set(-5, 0, 0); 46 | scene.add(light); 47 | scene.add(light.target); 48 | ``` 49 | ![alt text](image-4.png) 50 | 51 | 4. 点光源(PointLight) 52 | 53 | 从一个点朝各个方向发射出光线的一种光照效果 54 | 55 | ```js 56 | const color = 0xFFFFFF; 57 | const intensity = 150; 58 | const light = new THREE.PointLight(color, intensity); 59 | light.position.set(0, 10, 0); 60 | scene.add(light); 61 | 62 | const helper = new THREE.PointLightHelper(light); 63 | scene.add(helper); 64 | ``` 65 | 66 | ![alt text](image-5.png) 67 | 68 | 5. 聚光灯(SpotLight) 69 | 70 | 一个点光源被一个圆锥体限制住了光照的范围 71 | 72 | 类似方向光(DirectionalLight)一样需要一个目标点,光源的位置是圆锥的顶点 73 | 74 | ```js 75 | const color = 0xFFFFFF; 76 | const intensity = 150; 77 | const light = new THREE.SpotLight(color, intensity); 78 | scene.add(light); 79 | scene.add(light.target); 80 | 81 | const helper = new THREE.SpotLightHelper(light); 82 | scene.add(helper); 83 | ``` 84 | 85 | ![alt text](image-6.png) 86 | 87 | 88 | 6. 矩形区域光(RectAreaLight) 89 | 90 | 矩形区域的发射出来的光照,例如长条的日光灯或者天花板上磨砂玻璃透进来的自然光 91 | 92 | 只能影响 MeshStandardMaterial 和 MeshPhysicalMaterial 93 | 94 | ```js 95 | const color = 0xFFFFFF; 96 | const intensity = 5; 97 | const width = 12; 98 | const height = 4; 99 | const light = new THREE.RectAreaLight(color, intensity, width, height); 100 | light.position.set(0, 10, 0); 101 | light.rotation.x = THREE.MathUtils.degToRad(-90); 102 | scene.add(light); 103 | 104 | const helper = new RectAreaLightHelper(light); 105 | light.add(helper); 106 | ``` 107 | 108 | ![alt text](image-7.png) 109 | 110 | 111 | * * * 112 | 113 | 先设置一下相机 114 | 115 | ```js 116 | const fov = 45; 117 | const aspect = 2; // canvas 的默认宽高 300:150 118 | const near = 0.1; 119 | const far = 100; 120 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 121 | camera.position.set(0, 10, 20); 122 | ``` 123 | 124 | 创建地平面用来打光 125 | 126 | ```js 127 | const planeSize = 40; 128 | 129 | const loader = new THREE.TextureLoader(); 130 | const texture = loader.load('resources/images/checker.png'); 131 | texture.wrapS = THREE.RepeatWrapping; // 设置重复模式 132 | texture.wrapT = THREE.RepeatWrapping; 133 | texture.magFilter = THREE.NearestFilter; // 采样模式 - 当一个纹理元素大于一个像素时,贴图如何采样 134 | texture.colorSpace = THREE.SRGBColorSpace; 135 | const repeats = planeSize / 2; // 重复的次数 136 | texture.repeat.set(repeats, repeats); 137 | 138 | const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize); 139 | const planeMat = new THREE.MeshPhongMaterial({ 140 | map: texture, 141 | side: THREE.DoubleSide, 142 | }); 143 | const mesh = new THREE.Mesh(planeGeo, planeMat); 144 | // 旋转90° 得到一个 XZ 平面 145 | mesh.rotation.x = Math.PI * -.5; 146 | scene.add(mesh); 147 | 148 | // 添加一个立方体和球体 用于打光 149 | { 150 | const cubeSize = 4; 151 | const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize); 152 | const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'}); 153 | const mesh = new THREE.Mesh(cubeGeo, cubeMat); 154 | mesh.position.set(cubeSize + 1, cubeSize / 2, 0); 155 | scene.add(mesh); 156 | } 157 | { 158 | const sphereRadius = 3; 159 | const sphereWidthDivisions = 32; 160 | const sphereHeightDivisions = 16; 161 | const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions); 162 | const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'}); 163 | const mesh = new THREE.Mesh(sphereGeo, sphereMat); 164 | mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0); 165 | scene.add(mesh); 166 | } 167 | ``` -------------------------------------------------------------------------------- /documents/几何体&顶点&材质/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | import models 7 | 8 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /documents/几何体&顶点&材质/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; 4 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 5 | 6 | const scene = new THREE.Scene(); 7 | scene.background = new THREE.Color("#000"); 8 | 9 | const camera = new THREE.PerspectiveCamera( 10 | 75, 11 | window.innerWidth / window.innerHeight, 12 | 0.1, 13 | 1000 14 | ); 15 | camera.position.set(5, 5, 5); 16 | camera.lookAt(0, 0, 0); 17 | // camera.position.z = 10; 18 | 19 | const renderer = new THREE.WebGLRenderer(); 20 | renderer.setSize(window.innerWidth, window.innerHeight); 21 | // 创建坐标辅助器 22 | const axesHelper = new THREE.AxesHelper(5); 23 | scene.add(axesHelper); 24 | 25 | // 创建鼠标控制器 26 | let controls = new OrbitControls(camera, renderer.domElement); 27 | controls.addEventListener("change", function () { 28 | renderer.render(scene, camera); //执行渲染操作 29 | }); 30 | 31 | // 创建几何体 32 | const geometry = new THREE.BufferGeometry(); 33 | 34 | // 创建顶点数据,以三个坐标为一个顶点,逆时针为正面 35 | // const vertices = new Float32Array([ 36 | // -1.0, -1.0, 0.0, 37 | // 1.0, -1.0, 0.0, 38 | // 1.0, 1.0, 0.0, 39 | // 1.0, 1.0, 0.0, 40 | // -1.0, 1.0, 0.0, 41 | // -1.0, -1.0, 0.0, 42 | // ]); 43 | // // 创建顶点属性 44 | // geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3)); 45 | // console.log(geometry); 46 | 47 | // 使用索引绘制 48 | const vertices2 = new Float32Array([ 49 | -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 50 | ]); 51 | // 创建索引 52 | const indices = new Uint16Array([0, 1, 2, 2, 3, 0]); 53 | geometry.setIndex(new THREE.BufferAttribute(indices, 1)); 54 | geometry.setAttribute("position", new THREE.BufferAttribute(vertices2, 3)); 55 | console.log(geometry); 56 | 57 | // 设置2个顶点组,形成2个材质 58 | geometry.addGroup(0, 3, 0); 59 | geometry.addGroup(3, 3, 1); 60 | 61 | // 创建材质 62 | const material = new THREE.MeshBasicMaterial({ 63 | color: 0x00ff00, 64 | side: THREE.DoubleSide, 65 | wireframe: true, 66 | }); 67 | const material1 = new THREE.MeshBasicMaterial({ color: 0xff0000 }); 68 | const cube = new THREE.Mesh(geometry, [material, material1]); 69 | scene.add(cube); 70 | 71 | const boxGeometry = new THREE.BoxGeometry(1, 1, 1); 72 | const boxMaterial1 = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 73 | const boxMaterial2 = new THREE.MeshBasicMaterial({ color: 0xff0000 }); 74 | const boxMaterial3 = new THREE.MeshBasicMaterial({ color: 0x0000ff }); 75 | const boxMaterial4 = new THREE.MeshBasicMaterial({ color: 0xff00ff }); 76 | const boxMaterial5 = new THREE.MeshBasicMaterial({ color: 0x00ffff }); 77 | const boxMaterial6 = new THREE.MeshBasicMaterial({ color: 0xffff00 }); 78 | const boxMaterials = [boxMaterial1, boxMaterial2, boxMaterial3, boxMaterial4, boxMaterial5, boxMaterial6]; 79 | const boxCube = new THREE.Mesh(boxGeometry, boxMaterials); 80 | boxCube.position.set(2, 0, 0); 81 | scene.add(boxCube); 82 | 83 | document.body.appendChild(renderer.domElement); 84 | 85 | window.onresize = () => { 86 | camera.aspect = window.innerWidth / window.innerHeight; 87 | renderer.setSize(window.innerWidth, window.innerHeight); 88 | camera.updateProjectionMatrix(); 89 | }; 90 | 91 | function animate() { 92 | controls.update(); 93 | renderer.render(scene, camera); 94 | requestAnimationFrame(animate); 95 | } 96 | animate(); 97 | -------------------------------------------------------------------------------- /documents/几何体&顶点&材质/几何体_顶点_索引_面_材质.md: -------------------------------------------------------------------------------- 1 | **几何体** 2 | 3 | 创建物体时通常是,创建一个几何体,再创建一个材质,最后合到一个网格中 进行渲染才是一个物体 4 | 5 | 几何体最基础的类就是 `BufferGeometry`。 6 | 7 | **顶点** 8 | 9 | 在 threejs 所有的面都是由三角形构成,三角形又三个顶点构成。 10 | 11 | 所有顶点数据都在 `geometry-attributes-position` 里。 12 | 13 | 顶点数据的类型为 32 位的浮点数 `Float32Array`。 14 | 15 | 可以使用索引复用顶点数据,减少顶点数。 16 | 17 | **分组** 18 | 19 | 可以使用 `addGroup` 对顶点进行分组 并指定使用的材质,例如将一个面的所有顶点分为一个 `group` 每个 `group` 单独渲染 20 | 21 | 几何体的分组数据在 `geometry-groups` 里 22 | -------------------------------------------------------------------------------- /documents/创建文字/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Creating a scene 7 | 8 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documents/创建文字/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; 3 | import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js"; 4 | import {OrbitControls} from "three/examples/jsm/controls/OrbitControls.js"; 5 | 6 | const scene = new THREE.Scene(); 7 | const camera = new THREE.PerspectiveCamera( 8 | 25, 9 | window.innerWidth / window.innerHeight, 10 | 1, 11 | 500 12 | ); 13 | const renderer = new THREE.WebGLRenderer(); 14 | renderer.setSize(window.innerWidth, window.innerHeight); 15 | camera.position.set(0, 0, 50); 16 | camera.lookAt(0, 0, 0); 17 | 18 | //创建鼠标控制器 19 | let controls = new OrbitControls(camera, renderer.domElement ); 20 | //监听控制器,每次拖动后重新渲染画面 21 | controls.addEventListener('change', function () { 22 | renderer.render(scene, camera); //执行渲染操作 23 | }); 24 | 25 | document.body.appendChild(renderer.domElement); 26 | 27 | var loader = new FontLoader(); 28 | loader.load("helvetiker_regular.typeface.json", function (font) { 29 | // 创建文本几何体 30 | var textGeometry = new TextGeometry("Hello, Three.js!", { 31 | font: font, 32 | size: 1, 33 | height: 0.5, 34 | curveSegments: 12, 35 | bevelEnabled: true, 36 | bevelThickness: 0.1, 37 | bevelSize: 0.1, 38 | bevelSegments: 5, 39 | }); 40 | 41 | // 创建材质 42 | var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 43 | 44 | // 创建文本网格 45 | var textMesh = new THREE.Mesh(textGeometry, material); 46 | scene.add(textMesh); 47 | 48 | // 渲染场景 49 | function animate() { 50 | requestAnimationFrame(animate); 51 | renderer.render(scene, camera); 52 | } 53 | 54 | animate(); 55 | }); 56 | -------------------------------------------------------------------------------- /documents/创建文字/创建文字.md: -------------------------------------------------------------------------------- 1 | 有时候可能在 Three.js 中使用文本,有以下几种方法: 2 | 3 | 1. DOM + CSS 4 | 通过绝对定位将元素定位在其他 z-index 的元素之上 5 | 6 | ```css 7 | #info { 8 | position: absolute; 9 | top: 10px; 10 | width: 100%; 11 | text-align: center; 12 | z-index: 100; 13 | display: block; 14 | } 15 | ``` 16 | 17 | 2. 将文字绘制到画布中,作为 texture 纹理 18 | 3. 在 3d 软件中建模,导出给 three.js 19 | 4. three.js 自带的文字几何体 20 | 请参阅 [TextGeometry](https://threejs.org/docs/index.html#examples/zh/geometries/TextGeometry) 页面来阅读如何完成此操作的详细信息,以及每一个接收的参数的描述 21 | -------------------------------------------------------------------------------- /documents/动画.md: -------------------------------------------------------------------------------- 1 | Threejs骨骼动画需要通过骨骼网格模型类SkinnedMesh来实现,一般来说骨骼动画模型都是3D美术创建,然后程序员通过threejs引擎加载解析。 2 | 3 | `SkinnedMesh`:具有Skeleton(骨架)和bones(骨骼)的网格,可用于给几何体上的顶点添加动画 4 | 5 | `Bone`:骨骼是Skeleton(骨架)的一部分。骨架是由SkinnedMesh(蒙皮网格)依次来使用的。 骨骼几乎和空白Object3D相同。 6 | 7 | 骨骼使用`SkinnedMesh`和`Bone`类型的数据 8 | 9 | ![alt text](image-8.png) 10 | 11 | animations是一个动画数组,其中包含着多个**AnimationClip**,每个AnimationClip都是一个完整的动画数据 12 | 13 | ![alt text](image-9.png) 14 | 15 | * blendMode 混合模式(暂未确定干什么用的,可能是这个,要翻墙 动画混合模式) 16 | * duration 动画时长,单位为秒(上面的数据就是4.76秒) 17 | * name 动画名称 18 | * tracks 一个由关键帧轨道(KeyframeTracks)组成的数组,这个是动画动起来的关键,我们可以简单理解为里面的数据告诉模型的是每个时间点每个顶点要在什么位置,按时间轴去改变顶点位置来形成动画。 19 | * uuid 实例的UUID,自动分配且不可编辑 20 | 21 | **AnimationMixer**对于传入的模型是一个全局的动画控制器,可以对动画进行一个全局的管理。 22 | 23 | ```js 24 | // 创建模型动作混合器 25 | const mixer = new THREE.AnimationMixer(model); 26 | ``` 27 | 28 | **mixer.clipAction**:AnimationMixer的一个方法,返回所传入的剪辑参数的`AnimationAction` 29 | 30 | ```js 31 | const action = mixer.clipAction(glb.animations[0]); 32 | ``` 33 | 34 | 只有AnimationAction对象才能播放暂停动画,所有的剪辑剪辑对象AnimationClip必须通过AnimationAction来调度。 35 | 36 | ```js 37 | action.play(); 38 | ``` -------------------------------------------------------------------------------- /documents/后期处理effectComposer/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; 4 | import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; 5 | import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; 6 | import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; 7 | 8 | function init() { 9 | // 1【渲染开始】创建了一个RenderPass对象,用于将场景渲染到纹理上。 10 | const renderScene = new RenderPass(scene, camera); 11 | 12 | // 2【需要合成的中间特效】创建了一个UnrealBloomPass对象,用于实现辉光效果。≈ 13 | const bloomPass = new UnrealBloomPass( 14 | new THREE.Vector2(window.innerWidth, window.innerHeight), 15 | 1.5, 16 | 0.4, 17 | 0.85 18 | ); 19 | // 【特效设置】设置发光参数,阈值、强度和半径。 20 | bloomPass.threshold = params.threshold; 21 | bloomPass.strength = params.strength; 22 | bloomPass.radius = params.radius; 23 | 24 | // 3【效果输出】创建了一个OutputPass对象,用于将最终渲染结果输出到屏幕上。 25 | const outputPass = new OutputPass(); 26 | 27 | // 4【特效合并】创建了一个EffectComposer对象,并将RenderPass、UnrealBloomPass和OutputPass添加到渲染通道中。 28 | composer = new EffectComposer(renderer); 29 | composer.addPass(renderScene); 30 | composer.addPass(bloomPass); 31 | composer.addPass(outputPass); 32 | } 33 | 34 | function animate() { 35 | requestAnimationFrame(animate); 36 | // 5【渲染特效】通过调用 render 方法,将场景渲染到屏幕上。 37 | composer.render(); 38 | } 39 | -------------------------------------------------------------------------------- /documents/后期处理effectComposer/readme.md: -------------------------------------------------------------------------------- 1 | https://threejs.org/examples/#webgl_postprocessing_unreal_bloom 2 | 3 | 后期处理:简单的说就是先渲染一张图存起来,在这张图上面"添油加醋",处理完后再渲染到屏幕上。这一过程three进行了封装,使用现成的可以更快实现需求 4 | 5 | 1. EffectComposer(渲染后处理的通用框架,用于将多个渲染通道(pass)组合在一起创建特定的视觉效果) 6 | 2. RenderPass(是用于渲染场景的通道。它将场景和相机作为输入,使用Three.js默认的渲染器(renderer)来进行场景渲染,并将结果输出给下一个渲染通道) 7 | 3. UnrealBloomPass(是 three.js 中用于实现泛光效果的后期处理效果,通过高斯模糊和屏幕混合技术,将亮度较高的区域扩散开来,从而实现逼真的泛光效果。) 8 | 4. ShaderPass(是一个自定义着色器的通道。它允许你指定自定义的着色器代码,并将其应用于场景的渲染结果。这样你可以创建各种各样的图形效果,如高斯模糊、后处理效果等) 9 | 10 | 11 | 几乎任何后期处理,都需要 EffectComposer.js 和 RenderPass.js。 12 | 13 | https://blog.csdn.net/shebao3333/article/details/133163262 常见的6种后期处理 -------------------------------------------------------------------------------- /documents/图元.md: -------------------------------------------------------------------------------- 1 | `Three.js` 有很多图元。图元就是一些 3D 的形状,在运行时根据大量参数生成。 2 | 3 | https://threejs.org/manual/#zh/primitives 4 | 5 | `Geometry` 和 `BufferGeometry` 是成对出现的。 这两种类型的区别是**高效灵活 vs 性能。** 6 | 7 | 基于 `BufferGeometry` 的图元是**面向性能**的类型。 顶点是一个高效的类型数组形式。 它们能**更快的启动,占用更少的内存**。但如果想修改数据,就需要复杂的编程。 8 | 9 | 基于 `Geometry` 的图元**更灵活、更易修改 但需要更多的内存**, 它们根据 JavaScript 的类而来。在能够被渲染前,**Three.js 会将它们转换成相应的 `BufferGeometry` 表现形式** 10 | 11 | > 总结:threejs提供了两种几何体类型,`BufferGeometry` 性能更高 操作较难 和 `Geometry` 内存占用高 操作轻松。 12 | 13 | ```js 14 | const material = new THREE.MeshPhongMaterial({ 15 | side: THREE.DoubleSide, 16 | }); 17 | ``` 18 | 可以把 `side: THREE.DoubleSide` 传给材质,绘制出图元的两个面。不然在某些二维图形上,从反面观察他们会消失。 19 | 20 | 但对于实心的形状,比如球体 立方体,通常不需要绘制三角形的背面(物体是由面组成 面由顶点组成三角形),因为他们全部朝向内部。 21 | 22 | 23 | 所有形状都有多个设置来设置它们的细化程度。 24 | 25 | 细分的越少,运行的越流畅,使用的内存也会更少 -------------------------------------------------------------------------------- /documents/场景图sceneGraph/class_AxisGridHelper.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | // 打开/关闭轴和网格的可见性 3 | // lil-gui 要求一个返回类型为bool型的属性 4 | // 来创建一个复选框,所以我们为 `visible`属性 5 | // 绑定了一个setter 和 getter。 从而让lil-gui 6 | // 去操作该属性. 7 | class AxisGridHelper { 8 | constructor(node, units = 10) { 9 | const axes = new THREE.AxesHelper(); 10 | axes.material.depthTest = false; 11 | axes.renderOrder = 2; // 在网格渲染之后再渲染 12 | node.add(axes); 13 | 14 | const grid = new THREE.GridHelper(units, units); 15 | grid.material.depthTest = false; 16 | grid.renderOrder = 1; 17 | node.add(grid); 18 | 19 | this.grid = grid; 20 | this.axes = axes; 21 | this.visible = false; 22 | } 23 | get visible() { 24 | return this._visible; 25 | } 26 | set visible(v) { 27 | this._visible = v; 28 | this.grid.visible = v; 29 | this.axes.visible = v; 30 | } 31 | } 32 | 33 | export { AxisGridHelper }; 34 | -------------------------------------------------------------------------------- /documents/场景图sceneGraph/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/场景图sceneGraph/image-1.png -------------------------------------------------------------------------------- /documents/场景图sceneGraph/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/场景图sceneGraph/image-2.png -------------------------------------------------------------------------------- /documents/场景图sceneGraph/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/场景图sceneGraph/image.png -------------------------------------------------------------------------------- /documents/场景图sceneGraph/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | import models 7 | 8 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /documents/场景图sceneGraph/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "../../node_modules/three/build/three.module.js"; 2 | import { OrbitControls } from "../../node_modules/three/examples/jsm/controls/OrbitControls.js"; 3 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 4 | import { AxisGridHelper } from "./class_AxisGridHelper.js"; 5 | 6 | const scene = new THREE.Scene(); 7 | scene.background = new THREE.Color("#000"); 8 | 9 | const camera = new THREE.PerspectiveCamera( 10 | 40, 11 | window.innerWidth / window.innerHeight, 12 | 0.1, 13 | 1000 14 | ); 15 | camera.position.set(0, 100, 0); 16 | camera.up.set(0, 0, 1); 17 | camera.lookAt(0, 0, 0); 18 | // camera.position.z = 10; 19 | 20 | const renderer = new THREE.WebGLRenderer(); 21 | renderer.setSize(window.innerWidth, window.innerHeight); 22 | // 创建坐标辅助器 23 | // const axesHelper = new THREE.AxesHelper(5); 24 | // axesHelper.scale.set(3, 3, 3); 25 | // axesHelper.material.depthTest = false; 26 | // axesHelper.renderOrder = 2; 27 | // scene.add(axesHelper); 28 | 29 | // 创建鼠标控制器 30 | let controls = new OrbitControls(camera, renderer.domElement); 31 | controls.addEventListener("change", function () { 32 | renderer.render(scene, camera); //执行渲染操作 33 | }); 34 | 35 | // 创建GUI 36 | const gui = new GUI(); 37 | 38 | // 添加光源 39 | { 40 | const color = 0xffffff; 41 | const intensity = 500; 42 | const light = new THREE.PointLight(color, intensity); 43 | scene.add(light); 44 | } 45 | 46 | // 记录需要旋转的对象数组 47 | const rotate_objects = []; 48 | 49 | { 50 | // 创建通用的球体 51 | const radius = 1; 52 | const widthSegments = 6; 53 | const heightSegments = 6; 54 | const sphere_geometry = new THREE.SphereGeometry( 55 | radius, 56 | widthSegments, 57 | heightSegments 58 | ); 59 | // 创建代表太阳系的局部坐标系 , 仅用于坐标系 本身没有材质和几何体 60 | const solarSystem = new THREE.Object3D(); 61 | scene.add(solarSystem); 62 | rotate_objects.push(solarSystem); 63 | 64 | // 模拟太阳的几何体 65 | const sun_material = new THREE.MeshPhongMaterial({ 66 | emissive: 0xffff00, 67 | }); 68 | const sun_mesh = new THREE.Mesh(sphere_geometry, sun_material); 69 | sun_mesh.scale.set(5, 5, 5); 70 | // scene.add(sun_mesh); 71 | solarSystem.add(sun_mesh); 72 | rotate_objects.push(sun_mesh); 73 | 74 | // 模拟地球的几何体 75 | const earth_material = new THREE.MeshPhongMaterial({ 76 | color: 0x2233ff, 77 | emissive: 0x112244, 78 | }); 79 | const earth_mesh = new THREE.Mesh(sphere_geometry, earth_material); 80 | // earth_mesh.position.x = 10; 81 | // scene.add(earth_mesh); 82 | // solarSystem.add(earth_mesh); 83 | rotate_objects.push(earth_mesh); 84 | // 将地球设置为太阳的子节点 但是这样scale.set会通用对地球生效 85 | // sun_mesh.add(earth_mesh); 86 | 87 | // 模拟地球的局部坐标系 88 | const earth_orbit = new THREE.Object3D(); 89 | earth_orbit.position.x = 10; 90 | earth_orbit.add(earth_mesh); 91 | solarSystem.add(earth_orbit); 92 | rotate_objects.push(earth_orbit); 93 | 94 | // 模拟月球的几何体 95 | const moon_orbit = new THREE.Object3D(); 96 | moon_orbit.position.x = 2; 97 | earth_orbit.add(moon_orbit); 98 | 99 | const moon_material = new THREE.MeshPhongMaterial({ 100 | color: 0x888888, 101 | emissive: 0x222222, 102 | }); 103 | const moon_mesh = new THREE.Mesh(sphere_geometry, moon_material); 104 | moon_mesh.scale.set(0.5, 0.5, 0.5); 105 | moon_orbit.add(moon_mesh); 106 | rotate_objects.push(moon_mesh); 107 | 108 | makeAxisGrid(solarSystem, "solarSystem", 25); 109 | makeAxisGrid(sun_mesh, "sun_mesh"); 110 | makeAxisGrid(earth_orbit, "earth_orbit"); 111 | makeAxisGrid(earth_mesh, "earth_mesh"); 112 | makeAxisGrid(moon_orbit, "moon_orbit"); 113 | makeAxisGrid(moon_mesh, "moon_mesh"); 114 | } 115 | 116 | // 为每个旋转的节点添加坐标系 117 | { 118 | rotate_objects.forEach((node) => { 119 | const axes = new THREE.AxesHelper(); 120 | axes.material.depthTest = false; 121 | axes.renderOrder = 1; 122 | node.add(axes); 123 | }); 124 | } 125 | 126 | function makeAxisGrid(node, label, units) { 127 | const helper = new AxisGridHelper(node, units); 128 | gui.add(helper, "visible").name(label); 129 | } 130 | 131 | document.body.appendChild(renderer.domElement); 132 | 133 | window.onresize = () => { 134 | camera.aspect = window.innerWidth / window.innerHeight; 135 | renderer.setSize(window.innerWidth, window.innerHeight); 136 | camera.updateProjectionMatrix(); 137 | }; 138 | 139 | function animate() { 140 | controls.update(); 141 | rotate_objects.forEach((obj) => { 142 | obj.rotation.y += 0.01; 143 | }); 144 | renderer.render(scene, camera); 145 | requestAnimationFrame(animate); 146 | } 147 | animate(); 148 | -------------------------------------------------------------------------------- /documents/场景图sceneGraph/场景图.md: -------------------------------------------------------------------------------- 1 | Three.js 的核心可以说是它的**场景图(scene graph)** 2 | 3 | 比如这样一个例子:太阳系、太阳、地球、月亮。 4 | 5 | 地球绕着太阳转,月球绕着地球转,月球绕着地球转了一圈。 6 | 7 | - 从月球的角度看,它是在地球的 "局部空间 "中旋转 8 | - 但月球相对于太阳的运动是一些疯狂的像螺线图一样的曲线 9 | - 但从月球的角度来看,它只需要关注自身围绕地球这个局部空间的旋转 10 | ![alt text](image.png) 11 | 12 | > 总结:我们要建立**有层次的局部坐标系** 给物体提供稳定的锚点 **只关心自己在局部坐标系中的位置**,否则我们的计算会非常困难 13 | > 14 | > 例如我们可以很简单的控制地球绕着太阳转,月亮绕着地球转 15 | > 但很难控制月亮绕着太阳转 16 | 17 | 比如跑道中存在一个汽车,当汽车移动时,我们不可能去单独计算每个轮子相对于跑道的位置,而是让轮子相对于汽车构成一个局部坐标系,在汽车移动时轮子跟着相对移动即可。 18 | 19 | ![alt text](image-1.png) 20 | 21 | 与此类似的还有世界中人类的每个手指、器官 相对于人这个模型物体形成局部坐标系。 22 | 23 | ![alt text](image-2.png) 24 | -------------------------------------------------------------------------------- /documents/归纳文档.md: -------------------------------------------------------------------------------- 1 | ## 常用术语 2 | 3 | - geometry: 几何体 —— 描述几何体的形状,如球体、立方体、平面、狗、猫、人、树、建筑等物体的顶点信息 4 | - material: 材质 —— 绘制几何体的表面属性,包括颜色、光泽度、纹理。一个材质可以使用多个纹理 5 | - mesh: 网格 —— 组合几何体和材质,可以理解为用一种特定的材质(Material)来绘制的一个特定的几何体(Geometry) 6 | - texture: 纹理 —— 将图像应用到几何体对象上,并通过调整纹理的属性来实现更丰富的视觉效果 7 | - camera: 相机 8 | - scene: 场景 —— 是 three.js 的基本的组成部分。需要 three.js 绘制的东西都需要加入到 scene 中 9 | - renderer: 渲染器 —— 会将摄像机视椎体中的三维场景渲染成一个二维图片显示在画布上 10 | - matrix4:矩阵,用于一个数学结构 描述平移、旋转和缩放等一系列操作后的总和,给物体设置完了记得`updateMatrixWorld`一下 11 | ```js 12 | // 通过矩阵乘法(matrix.multiply())合并多个变换操作 13 | // 矩阵 → 位置/旋转/缩放: 14 | const position = new THREE.Vector3(); 15 | const quaternion = new THREE.Quaternion(); 16 | const scale = new THREE.Vector3(); 17 | matrix.decompose(position, quaternion, scale); 18 | ``` 19 | - quaternion:四元数,用于描述物体的旋转方向(在底层图形 API(如 WebGL)中,旋转最终需转换为四元数),**欧拉角需先转换为四元数或矩阵,再应用到向量。** 20 | 21 | Three.js 没有提供 vector.applyEuler() 方法,因为欧拉角不能直接作用于向量旋转。 22 | ```js 23 | const matrix = new THREE.Matrix4().makeRotationFromQuaternion(quaternion); 24 | ``` 25 | - euler:欧拉角,绕三个坐标轴(比如X、Y、Z (α, β, γ))的旋转角度描述物体方向 26 | ```js 27 | const euler = new THREE.Euler(Math.PI/2, 0, 0, 'XYZ'); // 绕 X 轴旋转 90 度 28 | // 四元数 → 矩阵: 29 | const euler = new THREE.Euler(0, Math.PI/2, 0); 30 | const quaternion = new THREE.Quaternion().setFromEuler(euler); 31 | // 简单旋转:使用 Euler(如 object.rotation.x += 0.1)。 32 | ``` 33 | - cube: 立方体 34 | - vertices: 顶点 35 | - map: 贴图 36 | - vector: 矢量,向量 37 | - vector3: 三维向量,表示一个位置(THREE.Vector3(1, 2, 3)) 或者一个方向(THREE.Vector3(0, 1, 0)) 38 | ```js 39 | // 向量加法 40 | const v1 = new THREE.Vector3(1, 2, 3); 41 | const v2 = new THREE.Vector3(4, 5, 6); 42 | v1.add(v2); // v1 = (5,7,9) 43 | ``` 44 | - normal:法线,代表面的朝向,影响光线反射,始终垂直于某平面的直线 45 | - normalize:归一化,将一个向量的长度(模长)调整为 1,同时保持其方向不变的操作。 46 | - 任何需要纯方向或单位长度的场合(如光线、法线、朝向),都应先调用 .normalize() 47 | ```js 48 | // 运动方向:在速度计算中,方向向量归一化后,乘以标量速度值,确保物体移动速率恒定 49 | const direction = new THREE.Vector3(2, 3, 4).normalize(); 50 | const speed = 5; 51 | object.position.add(direction.multiplyScalar(speed)); // 精确移动5个单位 52 | ``` 53 | - segment: 段 —— 例如描述一个圆 水平、垂直的分段数 54 | - intensity: 强度 —— 设置光源的强度 55 | - roughness: 粗糙度 —— 设置材质的粗糙度,越粗糙反光效果越差,范围 0 到 1,比如 0 代表棒球 1 代表台球 56 | - shininess: 光泽度 —— 高光 specular 的强度 57 | - metalness: 金属度 —— 材质与金属的相似度 58 | - emissive: 发光 —— 设置材质的发光颜色 59 | - transmission: 透光率 0-1【1 表示透光最强,0 表示完全不透光】,需要搭配粗糙度 roughness 使用 60 | - pass: 通道 —— 用于对场景做一些后期处理效果,例如添加光晕、颗粒效果、色调、饱和度等 61 | - AnimationClip: 动画片段 —— 模型上的动画数据 62 | - Lensflare:镜头光晕 —— 创建一个模拟追踪着灯光的镜头光晕 63 | 64 | ## 通用代码 65 | 66 | **模板** 67 | 68 | ```js 69 | import * as THREE from "three"; 70 | 71 | const width = window.innerWidth; 72 | const height = window.innerHeight; 73 | const aspect = width / height; 74 | const fov = 45; 75 | 76 | const scene = new THREE.Scene(); 77 | scene.background = new THREE.Color("#eee"); 78 | 79 | const camera = new THREE.PerspectiveCamera(fov, aspect, 1, 1000); 80 | // 设置相机位置与方向 81 | camera.position.set(0, 0, 100); 82 | camera.lookAt(0, 0, 0); 83 | 84 | const renderer = new THREE.WebGLRenderer(); 85 | //设置渲染区域尺寸 86 | renderer.setSize(width, height); 87 | 88 | // 创建几何体、材质、网格 将网格添加到场景 89 | const geometry = new THREE.BoxGeometry(1, 1, 1); 90 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); 91 | const mesh = new THREE.Mesh(geometry, material); 92 | scene.add(mesh); 93 | 94 | document.body.appendChild(renderer.domElement); 95 | ``` 96 | 97 | **坐标辅助器:** 98 | 99 | https://threejs.org/docs/index.html#api/zh/helpers/AxesHelper 100 | 101 | ```js 102 | // 创建坐标辅助器 103 | const axesHelper = new THREE.AxesHelper(5); 104 | scene.add(axesHelper); 105 | ``` 106 | 107 | **位移、缩放、旋转、父子元素:** 108 | 109 | 我们放进场景的物体基本都是`Object3D`,threejs 中如果对象的类是继承自`Object3D`,那么就可以使用位移、缩放、旋转等功能,并且相互之间可以嵌套,在修改父元素时 子元素也会产生相对的变化。 110 | 111 | **`Object3D`** https://threejs.org/docs/index.html#api/zh/core/Object3D 112 | 113 | ```js 114 | //设置该向量的x、y 和 z 分量。 115 | mesh.position.set(x, y, z); 116 | //直接设置position的x,y,z属性 117 | mesh.position.x = x; 118 | mesh.position.y = y; 119 | mesh.position.z = z; 120 | 121 | //例如设置x轴放大3倍、y轴方向放大2倍、z轴方向不变 122 | cube.scale.set(3, 2, 1); 123 | //单独设置某个轴的缩放 124 | cube.scale.x = 3; 125 | 126 | //直接设置旋转属性,例如围绕x轴旋转90度 127 | cube.rotation.x = -Math.PI / 2; 128 | //围绕x轴旋转45度 129 | cube.rotation.set(-Math.PI / 4, 0, 0, "XZY"); 130 | ``` 131 | 132 | **轨道控制器:** 133 | 134 | https://threejs.org/docs/index.html#examples/zh/controls/OrbitControls 135 | 136 | ```js 137 | import { OrbitControls } from "../../node_modules/three/examples/jsm/controls/OrbitControls.js"; 138 | 139 | //创建鼠标控制器 140 | let controls = new OrbitControls(camera, renderer.domElement); 141 | // 设置自动旋转、设置阻尼效果 142 | controls.autoRotate = true; 143 | controls.enableDamping = true; 144 | //监听控制器,每次拖动后重新渲染画面 145 | controls.addEventListener("change", function () { 146 | renderer.render(scene, camera); //执行渲染操作 147 | }); 148 | 149 | // 角度和距离限制 150 | controls.maxAzimuthAngle = Math.PI; 151 | controls.minAzimuthAngle = Math.PI * 0.5; 152 | 153 | controls.maxPolarAngle = Math.PI; 154 | controls.minPolarAngle = Math.PI * 0.5; 155 | 156 | controls.maxDistance = 1000; 157 | controls.minDistance = 500; 158 | ``` 159 | 160 | **响应式画布:** 161 | 162 | ```js 163 | window.onresize = () => { 164 | camera.aspect = window.innerWidth / window.innerHeight; 165 | renderer.setSize(window.innerWidth, window.innerHeight); 166 | camera.updateProjectionMatrix(); 167 | }; 168 | ``` 169 | 170 | **加载模型:** 171 | 172 | https://www.cnblogs.com/tiandi/p/17064109.html 173 | 174 | https://blog.csdn.net/m0_56023096/article/details/135604100 175 | 176 | ```js 177 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; 178 | 179 | const loader = new GLTFLoader(); 180 | loader.load( 181 | "path/to/model.glb", 182 | function (gltf) { 183 | // gltf.scene.traverse((obj) => { 184 | // // 将图片作为纹理加载 185 | // let imgTexture = new THREE.TextureLoader().load("/textures/xxxx.png"); 186 | // // 调整纹理图的方向 187 | // imgTexture.flipY = false; 188 | // // 将纹理图生成材质 189 | // const material = new THREE.MeshBasicMaterial({ 190 | // map: imgTexture, 191 | // }); 192 | // obj.material = material; 193 | // }); 194 | scene.add(gltf.scene); 195 | }, 196 | undefined, 197 | function (error) { 198 | console.error(error); 199 | } 200 | ); 201 | ``` 202 | 203 | **GUI** 204 | 205 | lil-gui https://github.com/georgealways/lil-gui 206 | 207 | ```js 208 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 209 | 210 | const eventObj = { 211 | ResetCamera: function () { 212 | controls.reset(); 213 | }, 214 | myNumber: 1, 215 | myString: "lil-gui", 216 | }; 217 | 218 | // 创建GUI 219 | const gui = new GUI(); 220 | // 添加按钮 function类型案例 221 | gui.add(eventObj, "ResetCamera").name("重置"); 222 | 223 | // 控制数字 224 | const number_folder = gui.addFolder("number类型UI"); 225 | number_folder.add(eventObj, "myNumber", 0, 1); 226 | number_folder.add(eventObj, "myNumber", 0, 100, 2); // snap to even numbers 227 | number_folder.add(eventObj, "myNumber", [0, 1, 2]); 228 | number_folder.add(eventObj, "myNumber", { Label1: 0, Label2: 1, Label3: 2 }); 229 | 230 | // 创建控制菜单 231 | const folder = gui.addFolder("cube 位置"); 232 | // 控制物体位置 number类型案例 233 | folder 234 | .add(cube.position, "x", -5, 5) 235 | .step(1) 236 | .name("立方体X轴位置") 237 | .onChange((val) => { 238 | console.log("立方体X轴位置", val); 239 | }); 240 | folder 241 | .add(cube.position, "y") 242 | .min(-5) 243 | .max(5) 244 | .step(1) 245 | .name("立方体Y轴位置") 246 | .onFinishChange((val) => { 247 | console.log("立方体Y轴位置", val); 248 | }); 249 | 250 | // 线框模式 boolean类型案例 251 | gui.add(cube.material, "wireframe").name("线框模式"); 252 | 253 | // 颜色选择器 color类型案例 254 | const colorFormats = { 255 | cubeColor: "#ffffff", 256 | int: 0xffffff, 257 | object: { r: 1, g: 1, b: 1 }, 258 | array: [1, 1, 1], 259 | }; 260 | gui.addColor(colorFormats, "cubeColor").onChange((val) => { 261 | cube.material.color.set(val); 262 | }); 263 | 264 | gui.add(eventObj, "myString"); 265 | ``` 266 | 267 | **添加光源:** 268 | 269 | ```js 270 | /** 271 | * 光源设置 272 | */ 273 | //新建点光源(常用光源分为点光源和环境光,点光源的效果类似灯泡,环境光的效果类似白天的太阳光) 274 | var point = new THREE.PointLight(0xffffff); 275 | //设置点光源的位置 276 | point.position.set(400, 200, 300); 277 | //将点光源添加到场景中 278 | scene.add(point); 279 | ``` 280 | 281 | **支持阴影** 282 | 283 | ```js 284 | // 开启阴影贴图 285 | renderer.shadowMap.enabled = true; 286 | // 光能够制造阴影 287 | point.castShadow = true; 288 | // 接收阴影的地方 289 | mesh.receiveShadow = true; 290 | 291 | cubeMesh.castShadow = true; 292 | cubeMesh.receiveShadow = true; 293 | ``` 294 | 295 | **观察不可见对象:** 296 | 297 | ```js 298 | const helper = new THREE.DirectionalLightHelper(light); 299 | const helper = new THREE.PointLightHelper(light); 300 | const helper = new THREE.SpotLightHelper(light); 301 | const helper = new RectAreaLightHelper(light); 302 | scene.add(helper); 303 | ``` 304 | 305 | **加载纹理:** 306 | 307 | ```js 308 | // 创建纹理加载器 309 | const loader = new THREE.TextureLoader(); 310 | loader.load("../assets/grass.png", (texture) => { 311 | // 设置纹理的颜色空间 更符合人眼视觉 312 | texture.colorSpace = THREE.SRGBColorSpace; 313 | // 创建材质 314 | const lambert = new THREE.MeshLambertMaterial({ 315 | map: texture, 316 | }); 317 | const mesh = new THREE.Mesh(geometry, lambert); 318 | // 添加到场景 319 | scene.add(mesh); 320 | }); 321 | 322 | const loadManager = new THREE.LoadingManager(); 323 | const loader = new THREE.TextureLoader(loadManager); 324 | // 加载进度 325 | loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => { 326 | const progress = itemsLoaded / itemsTotal; 327 | progressBarElem.style.transform = `scaleX(${progress})`; 328 | }; 329 | // 加载完成 330 | loadManager.onLoad = () => { 331 | const cube = new THREE.Mesh(geometry, materials); 332 | scene.add(cube); 333 | cubes.push(cube); // 添加到我们要旋转的立方体数组中 334 | }; 335 | ``` 336 | 337 | **添加地板** 338 | 339 | ```js 340 | // 创建地板 341 | { 342 | const planeSize = 40; 343 | const loader = new THREE.TextureLoader(); 344 | const texture = loader.load( 345 | "https://threejsfundamentals.org/threejs/resources/images/checker.png" 346 | ); 347 | texture.wrapS = THREE.RepeatWrapping; 348 | texture.wrapT = THREE.RepeatWrapping; 349 | texture.magFilter = THREE.NearestFilter; 350 | texture.colorSpace = THREE.SRGBColorSpace; 351 | const repeats = planeSize / 2; 352 | texture.repeat.set(repeats, repeats); 353 | const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize); 354 | const planeMat = new THREE.MeshPhongMaterial({ 355 | map: texture, 356 | side: THREE.DoubleSide, 357 | }); 358 | const mesh = new THREE.Mesh(planeGeo, planeMat); 359 | mesh.receiveShadow = true; 360 | mesh.rotation.x = Math.PI * -0.5; 361 | scene.add(mesh); 362 | } 363 | ``` 364 | 365 | ## 启用和禁用图层 366 | 367 | camera.layers.enableAll(),启用所有图层 layers,enable 启用某个图层,disableAll 隐藏所有图层,toggle 禁用某个图层 368 | 369 | ## 设置像素比 370 | 371 | ```js 372 | renderer.setPixelRatio(window.devicePixelRatio); 373 | ``` 374 | 375 | 越低越模糊 376 | 377 | ## 相关网站 378 | 379 | windows 3d 查看器,自带资源库 380 | 381 | 官方文档 https://threejs.org/docs/index.html#manual/zh/introduction/ 382 | 383 | 官方文章 https://threejs.org/manual/#zh/fundamentals 384 | 385 | 官方推荐教程 https://threejs.org/docs/index.html#manual/zh/introduction/Useful-links 386 | 387 | sketch 模型网站 https://sketchfab.com/3d-models?features=downloadable&sort_by=-likeCount&cursor=cD0yOTk5 388 | 389 | 模型下载:sketchfab.com/ or market.pmnd.rs/ https://www.aplaybox.com/model/model 390 | 391 | 环境贴图 hdr 下载:hdrmaps.com/ https://polyhaven.com/hdris 392 | 393 | 贴图素材 https://www.aigei.com/view https://craftpix.net/ https://itch.io/ https://www.spriters-resource.com,https://www.sounds-resource.com https://opengameart.org/ 394 | 395 | blender https://blog.csdn.net/weixin_44121341/article/details/136684288 396 | 397 | 炫光效果 https://github.com/mrdoob/three.js/blob/master/examples/webgl_postprocessing_unreal_bloom.html 398 | 399 | ## 相关库 400 | 401 | 物理引擎/效果(Physics) Ammo.js、Oimo.js、enable3d、ammo.js、cannon-es、rapier、Jolt 402 | 403 | Geometry THREE.MeshLine 404 | 405 | 3D 文字和布局(3D Text and Layout)troika-three-text three-mesh-ui 406 | 407 | 粒子系统(Particle Systems) three.quarks three-nebula 408 | 409 | 逆向运动学(Inverse Kinematics) THREE.IK fullik closed-chain-ik 410 | 411 | 光线投射性能表现(Intersection and Raycast Performance)three-mesh-bvh 412 | 413 | 轨迹追踪(Path Tracing)three-gpu-pathtracer 414 | 415 | ## 常见问题 416 | 417 | ### 在窗口被重新调整大小的时候,如何保持大小不变 418 | 419 | 我们以一定的百分比增加了窗口的高度,那我们所想要的结果便是所有距离的可见高度都增加相同的百分比。 420 | 421 | 这并不能通过改变摄像机的位置来实现,相反,你得改变摄像机的 **视野角度(FOV)** 422 | 423 | ```js 424 | visible_height = 425 | 2 * Math.tan(((Math.PI / 180) * camera.fov) / 2) * distance_from_camera; 426 | ``` 427 | -------------------------------------------------------------------------------- /documents/材质.md: -------------------------------------------------------------------------------- 1 | Three.js提供了多种类型的 **材质(material)**。它们定义了对象在场景中的外型。你使用哪种材质取决于你想达到的目的 2 | 3 | 关于材质我们还要更多考虑到纹理,它为我们提供了大量选择。 4 | 5 | > 总结:材质有很多种类型,越真实的材质支持的功能越多 构建速度越慢,我们根据场景需要进行选择 6 | 7 | 材质的构建速度从最快到最慢: `MeshBasicMaterial` ➡ `MeshLambertMaterial` ➡ `MeshPhongMaterial` ➡ `MeshStandardMaterial` ➡ `MeshPhysicalMaterial` 8 | 9 | 可以在初始化时设置材质,也可以在实例化后设置材质 10 | ```js 11 | const material = new THREE.MeshPhongMaterial({ 12 | color: 0xFF0000, 13 | }); 14 | 15 | const material = new THREE.MeshPhongMaterial(); 16 | material.color.setHSL(0, 1, .5); // 红色 17 | ``` 18 | 19 | `material.flatShading` 是否对平面着色 20 | ![alt text](image.png) 21 | 22 | `material.side` 要显示三角形的哪个面。默认值是 THREE.FrontSide,其他选项有 THREE.BackSide 和 THREE.DoubleSide(正反两面) 23 | ![alt text](image-1.png) 24 | 25 | * `MeshBasicMaterial` 基础网格材质,不受光照的影响 26 | * `MeshLambertMaterial` 只在顶点计算光照 27 | * `MeshPhongMaterial` 则在每个像素计算光照、还支持**镜面高光** 28 | * `MeshStandardMaterial` 标准网格材质 使用**粗糙度和金属度** 29 | ![alt text](image-3.png) 30 | * `MeshPhysicalMaterial` 物理网格材质,相比标准材质增加了 **`clearcoat`和反射、折射** 31 | ![alt text](image-2.png) 32 | * `MeshDepthMaterial` 渲染每个像素的深度 33 | * `MeshNormalMaterial` 会显示几何体的法线 34 | 35 | > 在设置 flatShading 或者 添加/删除纹理之后,需要调用material.needUpdate = true,否则某些材质只会被渲染一次 -------------------------------------------------------------------------------- /documents/画线/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Creating a scene 7 | 8 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documents/画线/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import {OrbitControls} from "three/examples/jsm/controls/OrbitControls.js"; 3 | 4 | const scene = new THREE.Scene(); 5 | const camera = new THREE.PerspectiveCamera( 6 | 45, 7 | window.innerWidth / window.innerHeight, 8 | 1, 9 | 500 10 | ); 11 | const renderer = new THREE.WebGLRenderer(); 12 | renderer.setSize(window.innerWidth, window.innerHeight); 13 | // 摆设相机位置,设置视点方向 14 | camera.position.set(0, 0, 100); 15 | camera.lookAt(0, 0, 0); 16 | 17 | //创建鼠标控制器 18 | let controls = new OrbitControls(camera, renderer.domElement ); 19 | //监听控制器,每次拖动后重新渲染画面 20 | controls.addEventListener('change', function () { 21 | renderer.render(scene, camera); //执行渲染操作 22 | }); 23 | 24 | const material = new THREE.LineBasicMaterial({ color: 0x0000ff }); 25 | // 创建带有顶点的几何体 26 | const points = []; 27 | points.push(new THREE.Vector3(-10, 0, 0)); 28 | points.push(new THREE.Vector3(0, 10, 0)); 29 | points.push(new THREE.Vector3(10, 0, 0)); 30 | const geometry = new THREE.BufferGeometry().setFromPoints(points); 31 | 32 | const line = new THREE.Line(geometry, material); 33 | 34 | scene.add(line); 35 | 36 | document.body.appendChild(renderer.domElement); 37 | 38 | renderer.render(scene, camera); 39 | -------------------------------------------------------------------------------- /documents/相机camera/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/相机camera/image.png -------------------------------------------------------------------------------- /documents/相机camera/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | import models 7 | 8 | 32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /documents/相机camera/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; 3 | import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; 4 | 5 | function main() { 6 | 7 | const canvas = document.querySelector( '#c' ); 8 | const view1Elem = document.querySelector( '#view1' ); 9 | const view2Elem = document.querySelector( '#view2' ); 10 | const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } ); 11 | 12 | const fov = 45; 13 | const aspect = 2; // the canvas default 14 | const near = 5; 15 | const far = 100; 16 | const camera = new THREE.PerspectiveCamera( fov, aspect, near, far ); 17 | camera.position.set( 0, 10, 20 ); 18 | 19 | const cameraHelper = new THREE.CameraHelper( camera ); 20 | 21 | class MinMaxGUIHelper { 22 | 23 | constructor( obj, minProp, maxProp, minDif ) { 24 | 25 | this.obj = obj; 26 | this.minProp = minProp; 27 | this.maxProp = maxProp; 28 | this.minDif = minDif; 29 | 30 | } 31 | get min() { 32 | 33 | return this.obj[ this.minProp ]; 34 | 35 | } 36 | set min( v ) { 37 | 38 | this.obj[ this.minProp ] = v; 39 | this.obj[ this.maxProp ] = Math.max( this.obj[ this.maxProp ], v + this.minDif ); 40 | 41 | } 42 | get max() { 43 | 44 | return this.obj[ this.maxProp ]; 45 | 46 | } 47 | set max( v ) { 48 | 49 | this.obj[ this.maxProp ] = v; 50 | this.min = this.min; // this will call the min setter 51 | 52 | } 53 | 54 | } 55 | 56 | const gui = new GUI(); 57 | gui.add( camera, 'fov', 1, 180 ); 58 | const minMaxGUIHelper = new MinMaxGUIHelper( camera, 'near', 'far', 0.1 ); 59 | gui.add( minMaxGUIHelper, 'min', 0.1, 50, 0.1 ).name( 'near' ); 60 | gui.add( minMaxGUIHelper, 'max', 0.1, 50, 0.1 ).name( 'far' ); 61 | 62 | const controls = new OrbitControls( camera, view1Elem ); 63 | controls.target.set( 0, 5, 0 ); 64 | controls.update(); 65 | 66 | const camera2 = new THREE.PerspectiveCamera( 67 | 60, // fov 68 | 2, // aspect 69 | 0.1, // near 70 | 500, // far 71 | ); 72 | camera2.position.set( 40, 10, 30 ); 73 | camera2.lookAt( 0, 5, 0 ); 74 | 75 | const controls2 = new OrbitControls( camera2, view2Elem ); 76 | controls2.target.set( 0, 5, 0 ); 77 | controls2.update(); 78 | 79 | const scene = new THREE.Scene(); 80 | scene.background = new THREE.Color( 'black' ); 81 | scene.add( cameraHelper ); 82 | 83 | { 84 | 85 | const planeSize = 40; 86 | 87 | const loader = new THREE.TextureLoader(); 88 | const texture = loader.load( 'https://threejs.org/manual/examples/resources/images/checker.png' ); 89 | texture.wrapS = THREE.RepeatWrapping; 90 | texture.wrapT = THREE.RepeatWrapping; 91 | texture.magFilter = THREE.NearestFilter; 92 | texture.colorSpace = THREE.SRGBColorSpace; 93 | const repeats = planeSize / 2; 94 | texture.repeat.set( repeats, repeats ); 95 | 96 | const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize ); 97 | const planeMat = new THREE.MeshPhongMaterial( { 98 | map: texture, 99 | side: THREE.DoubleSide, 100 | } ); 101 | const mesh = new THREE.Mesh( planeGeo, planeMat ); 102 | mesh.rotation.x = Math.PI * - .5; 103 | scene.add( mesh ); 104 | 105 | } 106 | 107 | { 108 | 109 | const cubeSize = 4; 110 | const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize ); 111 | const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } ); 112 | const mesh = new THREE.Mesh( cubeGeo, cubeMat ); 113 | mesh.position.set( cubeSize + 1, cubeSize / 2, 0 ); 114 | scene.add( mesh ); 115 | 116 | } 117 | 118 | { 119 | 120 | const sphereRadius = 3; 121 | const sphereWidthDivisions = 32; 122 | const sphereHeightDivisions = 16; 123 | const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions ); 124 | const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } ); 125 | const mesh = new THREE.Mesh( sphereGeo, sphereMat ); 126 | mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 ); 127 | scene.add( mesh ); 128 | 129 | } 130 | 131 | { 132 | 133 | const color = 0xFFFFFF; 134 | const intensity = 3; 135 | const light = new THREE.DirectionalLight( color, intensity ); 136 | light.position.set( 0, 10, 0 ); 137 | light.target.position.set( - 5, 0, 0 ); 138 | scene.add( light ); 139 | scene.add( light.target ); 140 | 141 | } 142 | 143 | function resizeRendererToDisplaySize( renderer ) { 144 | 145 | const canvas = renderer.domElement; 146 | const width = canvas.clientWidth; 147 | const height = canvas.clientHeight; 148 | const needResize = canvas.width !== width || canvas.height !== height; 149 | if ( needResize ) { 150 | 151 | renderer.setSize( width, height, false ); 152 | 153 | } 154 | 155 | return needResize; 156 | 157 | } 158 | 159 | function setScissorForElement( elem ) { 160 | 161 | const canvasRect = canvas.getBoundingClientRect(); 162 | const elemRect = elem.getBoundingClientRect(); 163 | 164 | // compute a canvas relative rectangle 165 | const right = Math.min( elemRect.right, canvasRect.right ) - canvasRect.left; 166 | const left = Math.max( 0, elemRect.left - canvasRect.left ); 167 | const bottom = Math.min( elemRect.bottom, canvasRect.bottom ) - canvasRect.top; 168 | const top = Math.max( 0, elemRect.top - canvasRect.top ); 169 | 170 | const width = Math.min( canvasRect.width, right - left ); 171 | const height = Math.min( canvasRect.height, bottom - top ); 172 | 173 | // setup the scissor to only render to that part of the canvas 174 | const positiveYUpBottom = canvasRect.height - bottom; 175 | renderer.setScissor( left, positiveYUpBottom, width, height ); 176 | renderer.setViewport( left, positiveYUpBottom, width, height ); 177 | 178 | // return the aspect 179 | return width / height; 180 | 181 | } 182 | 183 | function render() { 184 | 185 | resizeRendererToDisplaySize( renderer ); 186 | 187 | // turn on the scissor 188 | renderer.setScissorTest( true ); 189 | 190 | // render the original view 191 | { 192 | 193 | const aspect = setScissorForElement( view1Elem ); 194 | 195 | // adjust the camera for this aspect 196 | camera.aspect = aspect; 197 | camera.updateProjectionMatrix(); 198 | cameraHelper.update(); 199 | 200 | // don't draw the camera helper in the original view 201 | cameraHelper.visible = false; 202 | 203 | scene.background.set( 0x000000 ); 204 | 205 | // render 206 | renderer.render( scene, camera ); 207 | 208 | } 209 | 210 | // render from the 2nd camera 211 | { 212 | 213 | const aspect = setScissorForElement( view2Elem ); 214 | 215 | // adjust the camera for this aspect 216 | camera2.aspect = aspect; 217 | camera2.updateProjectionMatrix(); 218 | 219 | // draw the camera helper in the 2nd view 220 | cameraHelper.visible = true; 221 | 222 | scene.background.set( 0x000040 ); 223 | 224 | renderer.render( scene, camera2 ); 225 | 226 | } 227 | 228 | requestAnimationFrame( render ); 229 | 230 | } 231 | 232 | requestAnimationFrame( render ); 233 | 234 | } 235 | 236 | main(); 237 | -------------------------------------------------------------------------------- /documents/相机camera/相机.md: -------------------------------------------------------------------------------- 1 | 在three.js中最常用的摄像机是 **透视摄像机 PerspectiveCamera** 2 | 3 | PerspectiveCamera通过四个属性来定义一个视锥。near定义了视锥的前端,far定义了后端,fov是视野,通过计算正确的高度来从摄像机的位置获得指定的以near为单位的视野 4 | 5 | ![alt text](image.png) 6 | 7 | 为什么near不设置0.000000001 far设置10000000,将一切尽收眼底呢? 8 | 9 | GPU没有足够的精度判断一个东西在前面还是后面。在默认情况下,离摄像机近的将会更清晰,离摄像机远的模糊 10 | 11 | 你需要选择**好好抉择near和far的设置,来和你的场景配合**。既不丢失重要的近景,也不让远处的东西消失不见 12 | 13 | 14 | 第二种常见的摄像机是**正交摄像机 OrthographicCamera**,和指定一个视锥不同的是,它需要设置left,right top,bottom,near,和far指定一个长方体 15 | 16 | 常见的用途是用OrthographicCamera来展示模型的三视图 -------------------------------------------------------------------------------- /documents/着色器shader/README.md: -------------------------------------------------------------------------------- 1 | # shader学习记录 2 | 3 | 学习记录自于 https://juejin.cn/post/7233359844974182437 4 | 5 | `console.log(geometry)` 打印几何体,可以看到其中 `attributes` 上携带的每个顶点的数据,里面包含: 6 | 1. **顶点坐标 position** 三维 顶点相对模型原点的坐标 7 | 2. **纹理坐标 uv** 二维 8 | 1. 纹理在模型的位置,比如流光的线条 可以通过移动纹理实现 9 | 3. **法线 normal** 三维 面的朝向,可以协助计算光线的入射与反射效果 10 | 11 | shader 里无法像 JavaScript 那样 console.log() 打印数据或 debug 调试,所以大家碰到问题可能会不知道如何解决,此时将数值转换成颜色,多通过图形实际的效果去理解和培养直觉或许是个不错的学习方式。 12 | 13 | ## vertex 14 | 15 | ### normal 16 | 17 | 希望每个顶点朝自己原本的方向去偏移,左侧的点往左,右侧的点往右,上方的点往上,下方的点往下......此时可以借助每个顶点自带的法线 normal 来达到这个目的 18 | 19 | 20 | ### position 21 | count 是顶点数,itemSize 是每个属性的维度数 22 | 23 | 比如 position 和 normal 都是3维的、uv 是2维的,具体说来就是 position 的 array 里是3个一组表示某个顶点的坐标数据 24 | 25 | 不管物体在哪个地方 position 都是几何体自身的坐标 26 | 27 | **改变 position = 改变几何体的形状** 28 | 29 | ### uv 30 | 31 | 32 | 33 | u 从左到右增加、v 从下到上增加,范围从左下角 (0, 0) 到右上角 (1, 1) 34 | 35 | ## 使用ShaderMaterial 36 | 37 | 用 `ShaderMaterial` 替换 `MeshBasicMaterial` 38 | 39 | 并且通过设置 `vertexShader` 顶点着色器和 `fragmentShader` 片元着色器,来实现自定义每个顶点、每个片元/像素如何显示 40 | 41 | * 在顶点(vertex)着色器里需要设置 `gl_Position` 顶点位置 42 | * 在片元(fragment)着色器里需要设置 `gl_FragColor` 片元/像素颜色 43 | * **GPU 分别对每个顶点、每个片元独立执行代码** 44 | ```js 45 | const vertex = ` 46 | void main() { 47 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 48 | } 49 | `; 50 | 51 | const fragment = ` 52 | void main() { 53 | gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 54 | } 55 | `; 56 | 57 | // const material = new THREE.MeshBasicMaterial({ color: 0x0ca678 }); 58 | const material = new THREE.ShaderMaterial({ 59 | vertexShader: vertex, 60 | fragmentShader: fragment 61 | }); 62 | ``` 63 | * **`position` 是几何体 attributes 里各顶点的坐标** 64 | * **`position` 需要先变成 `vec4` 四维向量类型,才能进行矩阵操作,实现旋转、平移、缩放等操作** 65 | * 乘以 **`modelMatrix`** 模型矩阵,将原本模型以自身本地坐标定位的方式变成世界坐标里适当的位置和大小 66 | * 乘以 **`viewMatrix`** 视图矩阵,实现物体基于相机的位置(因为相机位置、视角的不同看到的画面也会不同) 67 | * 以上合并称为 **`modelViewMatrix`** 68 | * 乘以 **`projectionMatrix`** 投影矩阵,变换到剪裁空间,最终变成二维屏幕上渲染出来的效果 69 | * **`modelViewMatrix`、`projectionMatrix` 和属性 `position` 都是 ShaderMaterial 里内置的可以直接拿来用** 70 | * shader 程序可以单独写在诸如 `vertex.glsl`、`fragment.glsl` 导入使用;也可以直接在 JavaScript 里用字符串格式表示 71 | 72 | ### uniforms 73 | 74 | 在 `ShaderMaterial` 里可以通过 `uniforms` 从主程序 js 里传入所需的变量,其在顶点着色器和片元着色器里都能获取到,且对于每个顶点或片元数值统一相同。 75 | 76 | ```c# 77 | const material = new THREE.ShaderMaterial({ 78 | uniforms: { 79 | uTime: 80 | { value: 0 } 81 | }, 82 | vertexShader: vertex, 83 | fragmentShader: fragment, 84 | }); 85 | 86 | // Animation 87 | material.uniforms.uTime.value = time; 88 | 89 | // fragment shader 90 | varying vec2 vUv; 91 | 92 | uniform float uTime; 93 | 94 | void main() { 95 | // 先居中,再绘制圆形 96 | float dist = length(vUv - vec2(0.5)); 97 | // 半径大小随时间周期变化 98 | float radius = 0.5 * (sin(uTime) * 0.5 + 0.5); 99 | vec3 color = vec3(step(radius, dist)); 100 | gl_FragColor = vec4(color, 1.0); 101 | } 102 | ``` 103 | 104 | ### extensions 105 | 106 | ### vertexShader顶点着色器 107 | 108 | **当每个顶点的变化不再步调一致、单调统一时,shader 的威力才开始显现。** 109 | 110 | 三维空间里的物体显示到二维屏幕上需要通过 MVP 矩阵变换操作 111 | 112 | 113 | 114 | 115 | ## GLSL语法 116 | 117 | ### 类型与运算规则 118 | * vec 向量类型系列,其中包含二维向量 vec2、三维向量 vec3、四维向量 vec4,可以分别看成由 (x,y)、(x,y,z)或(r,g,b)、(x,y,z,w)或(r,g,b,a) 等分量组成 119 | * 可以像下面代码里一样直接访问或修改对应分量数值; 120 | * 当分量的值都一样时,可以只写一个值; 121 | * 向量之间或向量与浮点数之间的加减乘除四则运算,都是基于每个分量单独计算的 122 | 123 | ```js 124 | // 浮点数 float、整型 int、布尔型 bool 125 | float alpha = 0.5; 126 | int num = 10; 127 | bool flag = true; 128 | 129 | vec2 a = vec2(1.0, 0.0); 130 | // a.x=1.0 a.y=0.0 131 | a.x = 2.0; 132 | a.y = 0.5; 133 | 134 | vec2 a = vec2(1.0); 135 | // a.x=1.0 a.y=1.0 136 | 137 | // 向量之间或向量与浮点数之间的加减乘除四则运算是基于每个分量单独计算 138 | vec2 a = vec2(1.0) + vec2(0.1, 0.2); 139 | // a.x=1.1 a.y=1.2 140 | vec2 a = vec2(1.0) * 2.0; 141 | // a.x=2.0 a.y=2.0 142 | 143 | vec3 b = vec3(1.0, 2.0, 0.0); 144 | // b.x=1.0 b.y=2.0 b.z=0.0 145 | // b.r=1.0 b.g=2.0 b.b=0.0 146 | b.z = 3.0; 147 | 148 | vec4 c = vec4(1.0, 1.0, 1.0, 1.0); 149 | // c.x=c.y=c.z=c.w=1.0 150 | // c.r=c.g=c.b=c.a=1.0 151 | c.r = 0.9 152 | c.g = 0.0; 153 | c.b = 0.0; 154 | 155 | vec3 d = vec3(vec2(0.5), 1.0); 156 | vec4 e = vec4(vec3(1.0), 1.0); 157 | vec4 e = vec4(vec2(0.3), vec2(0.1)); 158 | ``` 159 | 160 | ### 修饰符 161 | 在 `ShaderMaterial` 中 ,顶点坐标 position、纹理坐标 uv、法线向量 normal 都是顶点上的数据,已经被定义好了 162 | 163 | 如果使用 `RawShaderMaterial` 就需要手动声明 164 | 165 | * **`attribute` 修饰符表明这个数据是每个顶点上都不同** 166 | 167 | > 如每个顶点都不同的一个随机值 `aRandom` 168 | 169 | ```c# 170 | attribute float aRandom; 171 | ``` 172 | 173 | * **每个顶点或像素数值都相同时变量使用 `uniform` 修饰符** 174 | 175 | > 如传入统一的时间 `uTime` 176 | 177 | ```c# 178 | uniform float uTime; 179 | ``` 180 | 181 | * **`varying` 修饰符表示数据的传递**,在片元着色器里使用顶点着色器里的变量 182 | 183 | > 如果将 uv 从顶点着色器传递到片元着色器里 184 | 185 | ```c# 186 | // Vertex Shader 187 | attribute vec2 uv; 188 | 189 | varying vec2 vUv; 190 | 191 | void main() { 192 | vUv = uv; 193 | } 194 | 195 | // Fragment Shader 196 | varying vec2 vUv; 197 | 198 | void main() { 199 | gl_FragColor = vec4(vUv, 1.0, 1.0); 200 | } 201 | ``` 202 | 203 | ### 内置函数 204 | #### **`step(edge, x)` 函数** 205 | 206 | 返回0.0或1.0数值,如果 `xedge` 返回1.0 207 | ```c# 208 | float color = step(0.5, vUv.x); 209 | // 倒序 210 | float color = step(vUv.x, 0.5); 211 | ``` 212 | 213 | #### **`fract(float)` 函数** 214 | 215 | 取小数使得数值在 0.0-1.0 里循环重复,比如1.1、2.1取小数后都变回0.1 216 | 217 | ```c# 218 | // 重复 219 | gl_FragColor = vec4(vec3(fract(vUv.x * 3.0)), 1.0); 220 | // 条纹 221 | gl_FragColor = vec4(vec3(step(0.5, fract(vUv.x * 3.0))), 1.0); 222 | // 乘以正数 = 重复n次 223 | fract(float * n); 224 | ``` 225 | 226 | #### **`length(vec2)` 函数** 227 | 228 | 获取向量的长度,可用 `vUv` 计算每个像素离原点(0.0, 0.0)位置的距离 `dist`。 229 | 230 | 也可以用 **`distance()`** 函数计算两个点的距离来代替 `length()` 231 | 232 | ```c# 233 | float dist = length(vUv); 234 | // 减正数 = 中心点往右上偏移 235 | float dist = length(vUv - vec2(0.5)); 236 | ``` 237 | 238 | #### **`sin/cos/tan...` 三角函数** 239 | 240 | #### **`mix(x, y, a)` 函数** 241 | 242 | 线性插值,结果为 `x*(1-a)+y*a`,浮点数 a 的范围是0.0到1.0,根据其数值大小对 x、y 进行插值。 243 | 244 | ```c# 245 | // linear interpolation 线性插值 246 | mix(x, y, 0.0) => x 247 | mix(x, y, 1.0) => y 248 | mix(x, y, 0.5) => (x + y) / 2.0 249 | mix(x, y, a) => x * (1 - a) + y * a 250 | 251 | // 颜色渐变 252 | mix(color1, color2, vUv.x); 253 | x = color1 = vec3(1.0, 0.0, 0.0) = red 254 | y = color2 = vec3(0.0, 1.0, 0.0) = green 255 | a = vUv.x = 0.0 - 1.0 范围 256 | 257 | mix(x, y, a) = x * (1-a) + y * a 258 | 当 a = 0.25 => 259 | x * (1 - 0.25) + y * 0.25 260 | = x * 0.75 + y * 0.25 261 | = vec3(1.0, 0.0, 0.0) * 0.75 + vec3(0.0, 1.0, 0.0) * 0.25 262 | = vec3(0.75, 0.25, 0.00) // 分别对 rgb 分量套下公式得到对应数值 263 | ``` 264 | 265 | #### `smoothstep()` 内置函数 266 | 267 | `smoothstep(edge1, edge2, x)` 接收3个参数,当 x<=edge1 时返回0.0;当 x>=edge2 时返回1.0;当 edge1 2 | 3 | 4 | 5 | 6 | Creating a scene 7 | 8 | 16 | 17 |
18 | 19 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /documents/着色器shader/base/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | 4 | const scene = new THREE.Scene(); 5 | const camera = new THREE.PerspectiveCamera( 6 | 75, 7 | window.innerWidth / window.innerHeight, 8 | 0.1, 9 | 500 10 | ); 11 | const renderer = new THREE.WebGLRenderer({ 12 | antialias: true, 13 | alpha: true, 14 | }); 15 | renderer.setSize(window.innerWidth, window.innerHeight); 16 | camera.position.set(0, 0, 2.5); 17 | camera.lookAt(new THREE.Vector3()); 18 | scene.background = new THREE.Color("rgba(246, 241, 241, 0.23)"); 19 | 20 | let controls = new OrbitControls(camera, renderer.domElement); 21 | controls.addEventListener("change", function () { 22 | renderer.render(scene, camera); //执行渲染操作 23 | }); 24 | 25 | document.body.appendChild(renderer.domElement); 26 | 27 | // 渐变 28 | { 29 | // 创建平面几何体 30 | const planeGeometry = new THREE.PlaneGeometry(1, 1); 31 | 32 | // 创建shader 33 | const vertexShader = /* glsl */ ` 34 | varying vec2 v_uv; 35 | void main() { 36 | v_uv = uv; 37 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 38 | } 39 | `; 40 | 41 | const fragmenShader = /* glsl */ ` 42 | varying vec2 v_uv; 43 | void main() { 44 | float color_red = step(0.5, v_uv.y); 45 | gl_FragColor = vec4(v_uv, 0.0, 1); 46 | } 47 | `; 48 | 49 | // 创建材质 50 | const material = new THREE.ShaderMaterial({ 51 | fragmentShader: fragmenShader, 52 | vertexShader: vertexShader, 53 | side: THREE.DoubleSide, 54 | }); 55 | 56 | // 创建网格 57 | const mesh = new THREE.Mesh(planeGeometry, material); 58 | mesh.position.set(-0.6, -0.6, 0); 59 | scene.add(mesh); 60 | } 61 | 62 | // 突变 63 | { 64 | // 创建平面几何体 65 | const planeGeometry = new THREE.PlaneGeometry(1, 1); 66 | 67 | // 创建shader 68 | const vertexShader = /* glsl */ ` 69 | varying vec2 v_uv; 70 | void main() { 71 | v_uv = uv; 72 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 73 | } 74 | `; 75 | 76 | const fragmenShader = /* glsl */ ` 77 | varying vec2 v_uv; 78 | void main() { 79 | float color = step(v_uv.x, 0.5); 80 | gl_FragColor = vec4(vec3(color), 1); 81 | } 82 | `; 83 | 84 | // 创建材质 85 | const material = new THREE.ShaderMaterial({ 86 | fragmentShader: fragmenShader, 87 | vertexShader: vertexShader, 88 | side: THREE.DoubleSide, 89 | }); 90 | 91 | // 创建网格 92 | const mesh = new THREE.Mesh(planeGeometry, material); 93 | mesh.position.set(0.6, 0.6, 0); 94 | scene.add(mesh); 95 | } 96 | 97 | // 条纹 98 | { 99 | // 创建平面几何体 100 | const planeGeometry = new THREE.PlaneGeometry(1, 1); 101 | 102 | // 创建shader 103 | const vertexShader = /* glsl */ ` 104 | varying vec2 v_uv; 105 | void main() { 106 | v_uv = uv; 107 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 108 | } 109 | `; 110 | 111 | const fragmenShader = /* glsl */ ` 112 | varying vec2 v_uv; 113 | void main() { 114 | float color = step(fract(v_uv.x * 3.0), 0.5); 115 | gl_FragColor = vec4(vec3(color), 1.0); 116 | } 117 | `; 118 | 119 | // 创建材质 120 | const material = new THREE.ShaderMaterial({ 121 | fragmentShader: fragmenShader, 122 | vertexShader: vertexShader, 123 | side: THREE.DoubleSide, 124 | }); 125 | 126 | // 创建网格 127 | const mesh = new THREE.Mesh(planeGeometry, material); 128 | mesh.position.set(0.6, -0.6, 0); 129 | scene.add(mesh); 130 | } 131 | 132 | 133 | { 134 | // 创建平面几何体 135 | const planeGeometry = new THREE.BoxGeometry(1, 1, 1); 136 | 137 | // 创建shader 138 | const vertexShader = /* glsl */ ` 139 | varying vec2 v_uv; 140 | void main() { 141 | v_uv = uv; 142 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 143 | } 144 | `; 145 | 146 | const fragmenShader = /* glsl */ ` 147 | varying vec2 v_uv; 148 | void main() { 149 | float color = step(fract(v_uv.x * 3.0), 0.5); 150 | gl_FragColor = vec4(vec3(color), 1.0); 151 | } 152 | `; 153 | 154 | // 创建材质 155 | const material = new THREE.ShaderMaterial({ 156 | fragmentShader: fragmenShader, 157 | vertexShader: vertexShader, 158 | side: THREE.DoubleSide, 159 | }); 160 | 161 | // 创建网格 162 | const mesh = new THREE.Mesh(planeGeometry, material); 163 | mesh.position.set(-0.6, 0.6, 0); 164 | scene.add(mesh); 165 | } 166 | 167 | // 渲染场景 168 | function animate() { 169 | requestAnimationFrame(animate); 170 | renderer.render(scene, camera); 171 | } 172 | 173 | window.onresize = () => { 174 | camera.aspect = window.innerWidth / window.innerHeight; 175 | renderer.setSize(window.innerWidth, window.innerHeight); 176 | camera.updateProjectionMatrix(); 177 | }; 178 | 179 | animate(); 180 | -------------------------------------------------------------------------------- /documents/着色器shader/circle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | circle 7 | 8 | 16 | 17 |
18 | 19 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /documents/着色器shader/circle/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | 4 | const scene = new THREE.Scene(); 5 | const camera = new THREE.PerspectiveCamera( 6 | 75, 7 | window.innerWidth / window.innerHeight, 8 | 0.1, 9 | 500 10 | ); 11 | const renderer = new THREE.WebGLRenderer({ 12 | antialias: true, 13 | alpha: true, 14 | }); 15 | renderer.setSize(window.innerWidth, window.innerHeight); 16 | camera.position.set(0, 0, 2.5); 17 | camera.lookAt(new THREE.Vector3()); 18 | scene.background = new THREE.Color("rgba(246, 241, 241, 0.23)"); 19 | 20 | let controls = new OrbitControls(camera, renderer.domElement); 21 | controls.addEventListener("change", function () { 22 | renderer.render(scene, camera); //执行渲染操作 23 | }); 24 | 25 | document.body.appendChild(renderer.domElement); 26 | // 径向/动态/多组动态 27 | { 28 | // 创建平面几何体 29 | const planeGeometry = new THREE.PlaneGeometry(1, 1); 30 | 31 | // 创建shader 32 | const vertexShader = /* glsl */ ` 33 | varying vec2 v_uv; 34 | void main() { 35 | v_uv = uv; 36 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 37 | } 38 | `; 39 | 40 | // fract重复n次,0.707 是0.5(位移了0.5 + uv界限1.0)斜边的长度 41 | const fragmenShader = /* glsl */ ` 42 | varying vec2 v_uv; 43 | void main() { 44 | float distance = fract(length(v_uv - 0.5) / 0.707 * 5.0); 45 | vec3 color = vec3(step(0.5, distance)); 46 | gl_FragColor = vec4(color, 1.0); 47 | } 48 | `; 49 | 50 | // 创建材质 51 | const material = new THREE.ShaderMaterial({ 52 | fragmentShader: fragmenShader, 53 | vertexShader: vertexShader, 54 | side: THREE.DoubleSide, 55 | }); 56 | 57 | // 创建网格 58 | const mesh = new THREE.Mesh(planeGeometry, material); 59 | mesh.position.set(-0.6, 0.6, 0); 60 | scene.add(mesh); 61 | } 62 | 63 | { 64 | // 创建平面几何体 65 | const planeGeometry = new THREE.PlaneGeometry(1, 1); 66 | 67 | // 创建shader 68 | const vertexShader = /* glsl */ ` 69 | varying vec2 v_uv; 70 | void main() { 71 | v_uv = uv; 72 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 73 | } 74 | `; 75 | 76 | // sin(u_time) * 0.5 + 0.5 确保 范围在[0, 1],再 * 0.5 = 不是斜边线的最大距离 77 | const fragmenShader = /* glsl */ ` 78 | uniform float u_time; 79 | varying vec2 v_uv; 80 | void main() { 81 | float distance = length(v_uv - 0.5); 82 | float radius = 0.5 * (sin(u_time) * 0.5 + 0.5); 83 | vec3 color = vec3(step(radius, distance)); 84 | gl_FragColor = vec4(color, 1.0); 85 | } 86 | `; 87 | 88 | // 创建材质 89 | const material = new THREE.ShaderMaterial({ 90 | uniforms: { 91 | u_time: { value: 0 }, 92 | }, 93 | fragmentShader: fragmenShader, 94 | vertexShader: vertexShader, 95 | side: THREE.DoubleSide, 96 | }); 97 | 98 | // 创建网格 99 | const mesh = new THREE.Mesh(planeGeometry, material); 100 | mesh.position.set(0.6, 0.6, 0); 101 | scene.add(mesh); 102 | 103 | setInterval(() => { 104 | material.uniforms.u_time.value += 0.05; // 渲染场景 105 | }, 16); 106 | } 107 | function animate() { 108 | requestAnimationFrame(animate); 109 | renderer.render(scene, camera); 110 | } 111 | 112 | window.onresize = () => { 113 | camera.aspect = window.innerWidth / window.innerHeight; 114 | renderer.setSize(window.innerWidth, window.innerHeight); 115 | camera.updateProjectionMatrix(); 116 | }; 117 | 118 | animate(); 119 | -------------------------------------------------------------------------------- /documents/着色器shader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shader 示例 7 | 253 | 254 | 255 |
256 |
257 |

Shader 示例

258 | https://juejin.cn/user/1372654389433496/posts 261 |
262 | 263 |
264 |
265 |

List

266 |
267 |
268 |

* 渐变/突变/条纹

269 |

将 uv 纹理坐标传递到片元着色器里

270 |
271 |
272 | 273 |
274 |

+ 添加示例

275 |

点击这里添加您自己的Three.js示例到展示容器

276 |
277 | 278 |
279 |

💡 使用说明

280 |

1. 点击示例列表中的项目加载对应的Three.js场景

281 |

2. 在代码区域查看实现代码

282 |

3. 点击"添加您的示例"将您自己的代码添加到容器

283 |
284 |
285 | 286 |
287 | 292 | 295 |
296 |
297 | 298 |
299 |
300 |

Example Code

301 |
302 | 303 | 304 |
305 |
306 |
307 | // 旋转立方体示例 function initScene() { // 创建场景 const scene = new 308 | THREE.Scene(); scene.background = new THREE.Color(0x0a0a1a); // 309 | 310 |
311 |
312 | 313 | 365 | 366 | 367 | -------------------------------------------------------------------------------- /documents/纹理.md: -------------------------------------------------------------------------------- 1 | geometry 决定了物体内在的形状,而 material 决定了物体表面的效果,其中的关键就是使用`texture`纹理。 2 | 3 | `texture`纹理 允许我们将图像应用到几何体对象上,并通过**调整纹理的属性来实现更丰富的视觉效果**。 4 | 5 | 需要注意的是 **纹理往往是 threejs 中内存占用最多的部分** (宽度 _ 高度 _ 4 \* 1.33 字节),文件越小 下载速度越快,尺寸越小 内存占用越少。 6 | 7 | 通过纹理加载器`TextureLoader`来加载纹理文件。 8 | 9 | ```js 10 | // 创建纹理加载器 11 | const loader = new THREE.TextureLoader(); 12 | loader.load("../assets/grass.png", (texture) => { 13 | // 设置纹理的颜色空间 更符合人眼视觉 14 | texture.colorSpace = THREE.SRGBColorSpace; 15 | // 创建材质 16 | const lambert = new THREE.MeshLambertMaterial({ 17 | map: texture, 18 | }); 19 | const mesh = new THREE.Mesh(geometry, lambert); 20 | // 添加到场景 21 | scene.add(mesh); 22 | }); 23 | ``` 24 | 25 | 可以使用`LoadingManager`在加载纹理时获取进度 设置 loading 26 | 27 | ```js 28 | const loadManager = new THREE.LoadingManager(); 29 | const loader = new THREE.TextureLoader(loadManager); 30 | // 加载进度 31 | loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => { 32 | const progress = itemsLoaded / itemsTotal; 33 | progressBarElem.style.transform = `scaleX(${progress})`; 34 | }; 35 | // 加载完成 36 | loadManager.onLoad = () => { 37 | const cube = new THREE.Mesh(geometry, materials); 38 | scene.add(cube); 39 | cubes.push(cube); // 添加到我们要旋转的立方体数组中 40 | }; 41 | ``` 42 | 43 | 纹理存在两种滤镜模式:放大滤镜`Magnification Filters`和缩小滤镜`Minification Filters`,它们分别定义了 纹理大于1纹理元素 和 纹理小于或等于1纹理元素 44 | ```js 45 | // 放大滤镜 46 | THREE.NearestFilter 47 | THREE.LinearFilter 48 | 49 | // 缩小滤镜 50 | THREE.NearestFilter 51 | THREE.NearestMipmapNearestFilter 52 | THREE.NearestMipmapLinearFilter 53 | THREE.LinearFilter 54 | THREE.LinearMipmapNearestFilter 55 | THREE.LinearMipmapLinearFilter 56 | ``` 57 | 58 | 纹理可以重复、偏移和旋转 59 | ```js 60 | const timesToRepeatHorizontally = 4; 61 | const timesToRepeatVertically = 2; 62 | someTexture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically); 63 | 64 | const xOffset = .5; // offset by half the texture 65 | const yOffset = .25; // offset by 1/4 the texture 66 | someTexture.offset.set(xOffset, yOffset); 67 | 68 | someTexture.center.set(.5, .5); // 设置旋转中心 69 | someTexture.rotation = THREE.MathUtils.degToRad(45); 70 | ``` 71 | 72 | 纹理提供了包裹模式 基于属性`Wrapping S`和`Wrapping T` 73 | 74 | * THREE.ClampToEdgeWrapping 75 | 每条边上的最后一个像素无限重复。 76 | 77 | * THREE.RepeatWrapping 78 | 纹理重复 79 | 80 | * THREE.MirroredRepeatWrapping 81 | 在每次重复时将进行镜像 -------------------------------------------------------------------------------- /documents/贴图map/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Creating a scene 7 | 8 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documents/贴图map/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | import GUI from "three/examples/jsm/libs/lil-gui.module.min.js"; 4 | import { RGBELoader } from "three/examples/jsm/Addons.js"; 5 | 6 | const scene = new THREE.Scene(); 7 | scene.background = new THREE.Color("#000"); 8 | const camera = new THREE.PerspectiveCamera( 9 | 45, 10 | window.innerWidth / window.innerHeight, 11 | 1, 12 | 500 13 | ); 14 | const renderer = new THREE.WebGLRenderer(); 15 | renderer.setSize(window.innerWidth, window.innerHeight); 16 | // 摆设相机位置,设置视点方向 17 | camera.position.set(0, 0, 100); 18 | camera.lookAt(0, 0, 0); 19 | 20 | //创建鼠标控制器 21 | let controls = new OrbitControls(camera, renderer.domElement); 22 | //监听控制器,每次拖动后重新渲染画面 23 | controls.addEventListener("change", function () { 24 | renderer.render(scene, camera); //执行渲染操作 25 | }); 26 | 27 | // 创建纹理加载器 28 | let textureLoader = new THREE.TextureLoader(); 29 | const map = textureLoader.load("/textures/xxxx.png"); 30 | const aoMap = textureLoader.load("/textures/xxxx.png"); 31 | const alphaMap = textureLoader.load("/textures/xxxx.png"); 32 | const lightMap = textureLoader.load("/textures/xxxx.png"); 33 | const specularMap = textureLoader.load("/textures/xxxx.png"); 34 | // 创建场景加载器 35 | const rgbeLoader = new RGBELoader(); 36 | rgbeLoader.load("/textures/xxxx.hdr", (envMap) => { 37 | // 设置为环境映射模式 38 | envMap.mapping = THREE.EquirectangularReflectionMapping; 39 | // 设置为场景贴图 40 | scene.background = envMap; 41 | scene.environment = envMap; // 场景中没有环境贴图的物理材质 会以他为环境贴图 42 | // 设置材质的环境贴图 43 | planeMaterial.envMap = envMap; 44 | }); 45 | 46 | const planeGeometry = new THREE.PlaneGeometry(1, 1); 47 | const planeMaterial = new THREE.MeshBasicMaterial({ 48 | color: 0xffffff, 49 | map: map, 50 | transparent: true, 51 | aoMap: aoMap, 52 | alphaMap: alphaMap, 53 | lightMap: lightMap, 54 | specularMap: specularMap, 55 | reflectivity: 0.5, 56 | }); 57 | const plane = new THREE.Mesh(planeGeometry, planeMaterial); 58 | scene.add(plane); 59 | 60 | GUI.add(planeMaterial, "aoMapIntensity") 61 | .min(0) 62 | .max(1) 63 | .step(0.01) 64 | .name("环境遮罩光强度"); 65 | 66 | document.body.appendChild(renderer.domElement); 67 | 68 | renderer.render(scene, camera); 69 | -------------------------------------------------------------------------------- /documents/贴图map/贴图.md: -------------------------------------------------------------------------------- 1 | MeshBasicMaterial 是提供给物体最基础的材质(不受光照影响),标准材质受光照影响 2 | 3 | 在材质中为了让效果更好,我们可以使用贴图 4 | 5 | 贴图:物体材质的背景,可以设置透明度 6 | 透明贴图:黑跟白,控制贴图展示区域的亮度 7 | 环境贴图:使贴图反射周围的光,可以设置反射率 8 | 高光贴图:控制贴图不同位置反射的情况 9 | 光照贴图:将复杂的光照信息预先计算并存储为图像 10 | 环境光遮蔽贴图:由建模提供,更深度的优化阴影和光照 11 | 12 | 在 threejs 中我们需要以代码的方式,为材质设置这些贴图 13 | 14 | - `import { RGBELoader } from "three/examples/jsm/Addons.js";` 引入 HDR 加载器 15 | - `material.map` 设置贴图 16 | - `material.aoMap` 设置环境遮罩贴图 17 | - `material.aoMapIntensity` 设置环境遮罩贴图的强度 18 | - `material.alphaMap` 设置透明贴图 19 | - `material.lightMap` 设置光照贴图 20 | - `material.specularMap` 设置高光贴图 21 | - `material.envMap` 设置环境贴图 22 | - `material.reflectivity ` 设置反射强度 23 | - `MeshPhongMaterial.bumpMap` 设置凹凸贴图 24 | - `MeshPhongMaterial.normalMap ` 设置法线贴图 -------------------------------------------------------------------------------- /documents/阴影shadow/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/阴影shadow/image-1.png -------------------------------------------------------------------------------- /documents/阴影shadow/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/阴影shadow/image-2.png -------------------------------------------------------------------------------- /documents/阴影shadow/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/阴影shadow/image-3.png -------------------------------------------------------------------------------- /documents/阴影shadow/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/阴影shadow/image.png -------------------------------------------------------------------------------- /documents/阴影shadow/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | shadow 7 | 8 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documents/阴影shadow/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | 4 | const scene = new THREE.Scene(); 5 | scene.background = new THREE.Color("#000"); 6 | 7 | const camera = new THREE.PerspectiveCamera( 8 | 75, 9 | window.innerWidth / window.innerHeight, 10 | 0.1, 11 | 1000 12 | ); 13 | camera.position.set(0, 10, 15); 14 | 15 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 16 | renderer.setSize(window.innerWidth, window.innerHeight); 17 | renderer.shadowMap.enabled = true; 18 | 19 | document.body.appendChild(renderer.domElement); 20 | 21 | const controls = new OrbitControls(camera, renderer.domElement); 22 | controls.target.set(0, 5, 0); 23 | controls.update(); 24 | // controls.addEventListener("change", function () { 25 | // renderer.render(scene, camera); 26 | // }); 27 | 28 | // 创建地板 29 | { 30 | const planeSize = 40; 31 | const loader = new THREE.TextureLoader(); 32 | const texture = loader.load( 33 | "https://threejsfundamentals.org/threejs/resources/images/checker.png" 34 | ); 35 | texture.wrapS = THREE.RepeatWrapping; 36 | texture.wrapT = THREE.RepeatWrapping; 37 | texture.magFilter = THREE.NearestFilter; 38 | texture.colorSpace = THREE.SRGBColorSpace; 39 | const repeats = planeSize / 2; 40 | texture.repeat.set(repeats, repeats); 41 | const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize); 42 | const planeMat = new THREE.MeshPhongMaterial({ 43 | map: texture, 44 | side: THREE.DoubleSide, 45 | }); 46 | const mesh = new THREE.Mesh(planeGeo, planeMat); 47 | mesh.receiveShadow = true; 48 | mesh.rotation.x = Math.PI * -0.5; 49 | scene.add(mesh); 50 | } 51 | 52 | // 创建光源 53 | { 54 | const color = 0xffffff; 55 | const intensity = 100; 56 | const light = new THREE.PointLight(color, intensity); 57 | light.castShadow = true; 58 | light.position.set(0, 10, 0); 59 | scene.add(light); 60 | 61 | const helper = new THREE.PointLightHelper(light); 62 | scene.add(helper); 63 | } 64 | 65 | // 摆放球体和立方体 66 | { 67 | const cubeSize = 4; 68 | const geometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize); 69 | const material = new THREE.MeshPhongMaterial({ color: "#8ac" }); 70 | const cubeMesh = new THREE.Mesh(geometry, material); 71 | cubeMesh.receiveShadow = true; 72 | cubeMesh.castShadow = true; 73 | cubeMesh.position.set(cubeSize + 1, cubeSize / 2, 0); 74 | scene.add(cubeMesh); 75 | 76 | const sphereRadius = 3; 77 | const sphereWidthDivisions = 32; 78 | const sphereHeightDivisions = 16; 79 | const shpereGeo = new THREE.SphereGeometry( 80 | sphereRadius, 81 | sphereWidthDivisions, 82 | sphereHeightDivisions 83 | ); 84 | const sphereMat = new THREE.MeshPhongMaterial({ color: "#8ac" }); 85 | const sphereMesh = new THREE.Mesh(shpereGeo, sphereMat); 86 | sphereMesh.receiveShadow = true; 87 | sphereMesh.castShadow = true; 88 | sphereMesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0); 89 | scene.add(sphereMesh); 90 | } 91 | 92 | // 创建墙体用于接收阴影 93 | { 94 | const cubeSize = 30; 95 | const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize); 96 | const cubeMat = new THREE.MeshPhongMaterial({ 97 | color: "#CCC", 98 | side: THREE.BackSide, 99 | }); 100 | const mesh = new THREE.Mesh(cubeGeo, cubeMat); 101 | mesh.receiveShadow = true; 102 | mesh.position.set(0, cubeSize / 2 - 0.1, 0); 103 | scene.add(mesh); 104 | } 105 | function animation() { 106 | requestAnimationFrame(animation); 107 | renderer.render(scene, camera); 108 | } 109 | animation(); 110 | -------------------------------------------------------------------------------- /documents/阴影shadow/阴影.md: -------------------------------------------------------------------------------- 1 | Three.js 默认使用 **shadow maps(阴影贴图)** 2 | 3 | 阴影贴图的工作方式:光能够从自身开始给物体渲染阴影。(光和物体都要设置投射属性) 4 | 5 | 每个光源在投射阴影时都会重新绘制场景,多个灯光分别绘制多次。 6 | 常见解决方案:1. 允许多个光源,但只让一个光源投射阴影。 2. 使用光照贴图或者环境光贴图,但会导致静态光照 7 | 8 | 使用假阴影,例如在平面上放一个近似阴影的灰度纹理 9 | 10 | ![alt text](image.png) 11 | 12 | 有三种光可以投射阴影,分别为 **DirectionalLight 方向光**、 **PointLight 点光源**、**SpotLight 聚光灯** 13 | 14 | 有几个属性需要设置: 15 | 16 | ```js 17 | // 1. 设置渲染器中的阴影属性 18 | renderer.shadowMap.enabled = true; 19 | // 2. 设置光 能投射阴影 20 | const light = new THREE.DirectionalLight(color, intensity); 21 | light.castShadow = true; 22 | // 每个网格都能被设置是否 投射阴影 和 被投射阴影 23 | // 3. 设置网格(地面)能被投射阴影 24 | mesh.receiveShadow = true; 25 | // 4. 设置球体和立方体都能投射阴影 和 被投射阴影 26 | cubeMesh.castShadow = true; 27 | cubeMesh.receiveShadow = true; 28 | ``` 29 | 30 | * 将渲染器的shadowMapEnabled属性设置为true(告诉渲染器可以渲染隐形) 31 | * 将物体及光源的castShadow属性设置为true(告诉物体及光源可以透射阴影) 32 | * 将接收该阴影的物体的receiveShadow属性设置为true(告诉物体可以接收其他物体的阴影) 33 | 34 | 35 | **DirectionalLight 方向光**: 36 | 37 | 类似正交相机 38 | 39 | ![alt text](image-1.png) 40 | 41 | **SpotLight 聚光灯** 42 | 43 | 类似透视相机 44 | 45 | ![alt text](image-2.png) 46 | 47 | **PointLight 点光源** 48 | 49 | 只需设置near和far,相当于6个面的聚光灯组合而成,场景的阴影渲染6次。 50 | 51 | ![alt text](image-3.png) -------------------------------------------------------------------------------- /documents/雾fog/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XIwE1/threejs-study-notes/220d81d1e475e7283d25ad53dac8827efba3ec23/documents/雾fog/image.png -------------------------------------------------------------------------------- /documents/雾fog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fog 7 | 8 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documents/雾fog/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 3 | 4 | const scene = new THREE.Scene(); 5 | 6 | const camera = new THREE.PerspectiveCamera( 7 | 75, 8 | window.innerWidth / window.innerHeight, 9 | 0.1, 10 | 100 11 | ); 12 | camera.position.z = 2; 13 | 14 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 15 | renderer.setSize(window.innerWidth, window.innerHeight); 16 | renderer.shadowMap.enabled = true; 17 | 18 | document.body.appendChild(renderer.domElement); 19 | 20 | const controls = new OrbitControls(camera, renderer.domElement); 21 | controls.update(); 22 | 23 | 24 | { 25 | const color = 'lightblue'; 26 | const fog = new THREE.Fog(color, 1, 2); 27 | scene.fog = fog; 28 | scene.background = new THREE.Color(color); 29 | } 30 | { 31 | 32 | const color = 0xFFFFFF; 33 | const intensity = 3; 34 | const light = new THREE.DirectionalLight( color, intensity ); 35 | light.position.set( - 1, 2, 4 ); 36 | scene.add( light ); 37 | } 38 | 39 | const boxWidth = 1; 40 | const boxHeight = 1; 41 | const boxDepth = 1; 42 | const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth); 43 | 44 | function makeCubeInstance( geometry, color, x ) { 45 | const material = new THREE.MeshPhongMaterial( { color: color } ); 46 | const cubeMesh = new THREE.Mesh( geometry, material ); 47 | cubeMesh.position.x = x; 48 | scene.add( cubeMesh ); 49 | return cubeMesh; 50 | } 51 | 52 | const cubes = [ 53 | makeCubeInstance(geometry, 0x44aa88, 0), 54 | makeCubeInstance(geometry, 0x8844aa, -2), 55 | makeCubeInstance(geometry, 0xaa8844, 2), 56 | ]; 57 | 58 | function animation() { 59 | requestAnimationFrame(animation); 60 | cubes.forEach((cube) => { 61 | cube.rotation.x += 0.01; 62 | cube.rotation.y += 0.01; 63 | }) 64 | renderer.render(scene, camera); 65 | } 66 | animation(); 67 | -------------------------------------------------------------------------------- /documents/雾fog/雾.md: -------------------------------------------------------------------------------- 1 | 雾通常是基于 **离摄像机的指定距离褪色至某种特定颜色的方式** 2 | 3 | 添加雾是通过创建 **Fog** 或者 **FogExp2** 实例并设定**scene的fog属性** 4 | 5 | 设定 near 和 far 属性,代表距离摄像机的距离。 6 | 7 | 在 near 和 far 中间的物体,会从它们自身材料的颜色褪色到雾的颜色 8 | 9 | ```js 10 | const scene = new THREE.Scene(); 11 | { 12 | const color = 0xFFFFFF; // white 13 | const near = 10; 14 | const far = 100; 15 | scene.fog = new THREE.Fog(color, near, far); 16 | } 17 | ``` 18 | 19 | ![alt text](image.png) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | index 7 | 8 | 17 | 18 |
19 |

document

20 |
21 | 33 |
34 |

demo

35 |
36 |
    37 |
  • minecraft demo

  • 38 |
  • lineSketch demo

  • 39 |
40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-study-notes", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@types/three": "^0.164.1", 13 | "vite": "^5.2.0" 14 | }, 15 | "dependencies": { 16 | "@gltf-transform/cli": "^4.0.8", 17 | "global": "^4.4.0", 18 | "three": "^0.164.1" 19 | } 20 | } 21 | --------------------------------------------------------------------------------