three.js 学习笔记

什么是 three.js?

一个 JavaScript 库(引擎),基于 WebGL 进行封装

WebGL:浏览器提供的一个 API,实现 3D 效果在浏览器上

原因:WebGL 需要自己编写 shader 着色器语法(图形学+数学基础),而 three.js 对图形学和数学的代码进行了封装,上手更容易

image-20240918045510997

官网

https://threejs.org/

image-20240918051715780

three.js 三要素:

场景(世界),

摄像机(眼睛),

渲染器(把世界物体,眼睛传入,让它计算从哪个角度看到物体的哪个面画面,然后渲染到 canvas *面页面上)

image-20240918060055657

three.js 底层原理:

基于 canvas 标签绘制像素点(位置,颜色等)在*面上计算出3D效果

// 1.下载并引入 three 库
import * as THREE from 'three'

// 2. three.js三要素
// 2.1 场景:放置物体的容器世界
const scene = new THREE.Scene();
// 设置场景世界的背景色
scene.background = new THREE.Color(0x0f151e);

// 2.2 摄像机:类似人眼,决定看到多大范围空间
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

// 2.3 渲染器
// const renderer = new THREE.WebGLRenderer();
// antialias: true 抗锯齿处理(让渲染的图形边更光滑)
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// 3. 把场景+摄像机,传递给渲染器,渲染
renderer.render( scene, camera );

绘制three.js 立方体

geometry /dʒiˈɒmətri $ -ˈɑːm-/ noun 几何学

geometric /ˌdʒiːəˈmetrɪk◂/

(also geometrical /-trɪkəl/) adjective 几何图形的

material1 /məˈtɪəriəl $ -ˈtɪr-/ noun 料子,衣料,布料; 材料〔如木材、塑料、金属等〕; 材料 ;〔书、电影等中的〕素材

material2 ●○○ adjective 物质上的,非精神上的

image-20241003062844458

// 1.下载并引入 three 库
import * as THREE from 'three'

// 2. three.js 三要素
// 2.1 场景:放置物体的容器世界
const scene = new THREE.Scene();

// 2.2 摄像机:类似人眼,决定看到多大范围空间
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

// 2.3 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// 4. 绘制立方体
// 4.1 几何图形 BoxGeometry 立方体(宽,高,深)
//     1 单位大小(three.js 独有单位)
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
// 4.2 材质(皮肤):决定我要一个什么样子的立方体(颜色)
// MeshBasicMaterial 网格(面)基础(纯颜色)材质(0x 16进制颜色)
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
// 4.3 创建物体(Mesh)网格材质(面)
const cube = new THREE.Mesh( geometry, material );
// 4.4 把立方体加入到场景世界中
scene.add( cube );

// 4.5 把摄像机往 z 轴正方向移动 5 个单位远
camera.position.z = 5;

// 3. 把场景+摄像机,传递给渲染器,渲染
renderer.render( scene, camera );

摄像机——概念解释

透视摄像机:

角度:眼睛垂直睁开角度

截面:决定了能看到的范围大小(在截面以外的物体不会被渲染)

宽高比:决定了视椎体的范围大小

摄像机参数解释

透视摄像机:

角度:眼睛垂直睁开角度

截面:决定了能看到的范围大小(在截面以外的物体不会被渲染)

宽高比:决定了视椎体的范围大小

// 2.2 摄像机:类似人眼,决定看到多大范围空间
// PerspectiveCamera 透视摄像机(模拟人眼感觉-*大远小)
// 学习方法:通过尝试和数据切换,暂时总结表面效果——》上手
// 参数1:视野角度,决定看到物体范围,角度越小(物体越大),角度越大(物体越小) —— 官方文档推荐 75度 垂直角度比较贴*现实
// 参数2: 宽高比(和画布的宽高比要重度关系)
// 画布宽高比 = 摄像机宽高比(保证绘制出来的物体 1 比 1 正好的)
// 参数3:*截面距离(距离摄像机位置)
// 参数4: 远截面距离
// 在截面以外的物体是不会被渲染的
// 4 个参数决定了摄像机能看到多大范围内物体
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

image-20241003070731306

image-20241004162823792

aspect /ˈæspekt/ ●●● S2 W1 AWL noun
1  [countable] one part of a situation, idea, plan etc that has many parts 方面

aspect of
Dealing with people is the most important aspect of my work.
与人打交道是我工作中最重要的一方面。
Alcoholism affects all aspects of family life.
酗酒影响家庭生活的各个方面。
2  [countable] the direction in which a window, room, front of a building etc faces 朝向,方位
a south-facing aspect
朝南方向
3  [singular, uncountable] literary the appearance of someone or something 外表,外观
The storm outside gave the room a sinister aspect.
外面的暴风雨使房间里变得阴森可怕。
4  [countable, uncountable] technical the form of a verb in grammar that shows whether an action is continuing, or happens always, again and again, or once 〔动词的〕体〔表示动作正在进行、总是发生、反复发生或发生一次〕
‘He sings’ differs from ‘He is singing’ in aspect.

添加坐标轴 AxesHelper

axis /ˈæksɪs/ ●○○ noun (plural axes /-siːz/) [countable]

  1. the imaginary line around which a large round object, such as the Earth, turns 轴,轴线
    The Earth rotates on an axis between the north and south poles.
    地球绕着南北两极之间的地轴自转。

  2. a line drawn across the middle of a regular shape that divides it into two equal parts 〔将规则形状*分为二的〕对称轴,对称中心线

  3. either of the two lines of a graph, by which the positions of points are measured 参考轴线,坐标轴
    the vertical/horizontal axis 纵/横(坐标)轴

坐标轴:

用三条线模拟 x,y,z 坐标

世界坐标轴:scene.add(坐标轴)

物体/模型坐标轴:cube.add(坐标轴)

// 5. 添加坐标轴
function createAxes() {
    // 参数:坐标轴长度
    const axesHelper = new THREE.AxesHelper(5);
    // 坐标轴也是一个物体需要加入到场景世界中的显示
    // 物体虽然是集合图像+材质皮肤决定,坐标轴构造函数里决定好线段图形和颜色
    // x红线,y绿线,z蓝线
    scene.add(axesHelper);
}

createAxes();

image-20241005062106431

image-20241005061854237

轨道控制器(控制摄像机位置、角度)

控制摄像机的位置和角度等,从而渲染看到的不同画面

渲染循环:renderer.render(scene, camera)

原因:摄像机改变了位置/角度,需要 render 重新执行重新渲染最新画面,不断调度 render (放在渲染循环中)

渲染循环:用 requestAnimationFrame (根据浏览器刷新帧率,不断递归调用传入的函数)

负责改变摄像机位置,角度,远*。

从而观察世界不同角度不同画面。

  1. 引入附加组件

    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    
  2. 创建轨道控制器

    function createOrbit() {
        // 参数1:控制的摄像机
        // 参数2:鼠标交互的标签(监听canvas画布的鼠标的滑动交互)
        controls = new OrbitControls(camera, renderer.domElement);
        // 阻尼效果(缓慢的停止转动摄像机)
        controls.enableDamping = true;
        // 阻尼的系数(缓慢的速度)
        controls.dampingFactor = 0.125;
    }
    
  3. 使用渲染循环

    function animate() {
        // 根据浏览器刷新帧率(默认1s60次)不断递归调用此渲染函数,为了让renderer不断渲染最新画面
        requestAnimationFrame(animate);
    
        // 轨道控制器属性改变,需要更新轨道控制器才有效果
        controls.update();
    
        // 把场景+摄像机,传递给渲染器,渲染
        renderer.render(scene, camera);
    }
    

控制摄像机的位置和角度等,从而渲染看到的不同画面

渲染循环:renderer.render(scene, camera)

原因:摄像机改变了位置/角度,需要 render 重新执行重新渲染最新画面,不断调度 render (放在渲染循环中)

渲染循环:用 requestAnimationFrame (根据浏览器刷新帧率,不断递归调用传入的函数)

// 根据浏览器刷新帧率(默认1s60次)不断递归调用渲染函数,为了让render不断渲染最新画面
requestAnimationFrame(animate);

orbit /ˈɔːbɪt $ ˈɔːr-/ n.轨道 v.沿轨道运行;环绕…运行

damp1 /dæmp/ ●●○ adjective
1  slightly wet, often in an unpleasant way 潮湿的

damp2 noun [uncountable] British English
1 water in walls or in the air that causes things to be slightly wet 潮湿;湿气
Damp had stained the walls.
墙上留下了潮湿的痕迹。

damp3 verb [transitive]
1.to dampen something 使潮湿
2.damp something ↔ down phrasal verb to make a fire burn more slowly, often by covering it with ash 减弱〔火势〕,〔常指用盖灰的方法〕封〔火〕

操作鼠标,变动摄像机位置:

鼠标滚轮滚动,拉远拉*摄像机;

鼠标左键,旋转摄像机;

鼠标右键,*移摄像机。

image-20241020074353564

image-20241005064358622

// 1.下载并引入 three 库
import * as THREE from 'three'
// 6. Orbit controls(轨道控制器)
// 6.1 OrbitControls 是一个附加组件,必须显式导入
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 声明轨道控制器
let controls;

// 2. three.js 三要素
// 2.1 场景:放置物体的容器世界
const scene = new THREE.Scene();

// 2.2 摄像机:类似人眼,决定看到多大范围空间
// PerspectiveCamera 透视摄像机(模拟人眼感觉-*大远小)
// 学习方法:通过尝试和数据切换,暂时总结表面效果——》上手
// 参数1:视野角度,决定看到物体范围,角度越小(物体越大),角度越大(物体越小) —— 官方文档推荐 75度 垂直角度比较贴*现实
// 参数2: 宽高比(和画布的宽高比要重度关系)
// 画布宽高比 = 摄像机宽高比(保证绘制出来的物体 1 比 1 正好的)
// 参数3:*截面距离(距离摄像机位置)
// 参数4: 远截面距离
// 在截面以外的物体是不会被渲染的
// 4 个参数决定了摄像机能看到多大范围内物体
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

// 2.3 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// 4. 绘制立方体
// 4.1 几何图形 BoxGeometry 立方体(宽,高,深)
//     1 单位大小(three.js 独有单位)
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
// 4.2 材质(皮肤):决定我要一个什么样子的立方体(颜色)
// MeshBasicMaterial 网格(面)基础(纯颜色)材质(0x 16进制颜色)
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
// 4.3 创建物体(Mesh)网格材质(面)
const cube = new THREE.Mesh( geometry, material );
// 4.4 把立方体加入到场景世界中
scene.add( cube );

// 4.5 把摄像机往 z 轴正方向移动 5 个单位远
camera.position.z = 5;

// 5. 添加坐标轴
function createAxes() {
    // 参数:坐标轴长度
    const axesHelper = new THREE.AxesHelper(5);
    // 坐标轴也是一个物体需要加入到场景世界中的显示
    // 物体虽然是集合图像+材质皮肤决定,坐标轴构造函数里决定好线段图形和颜色
    // x红线,y绿线,z蓝线
    scene.add(axesHelper);
}


// 6.2 创建轨道控制器(控制摄像机位置)
function createOrbit() {
    // 参数1:控制的摄像机
    // 参数2:鼠标交互的标签(监听canvas画布鼠标滑动交互)
    controls = new OrbitControls(camera, renderer.domElement);
}

// 6.3 使用渲染循环
function animate() {
    // 根据浏览器刷新帧率(默认1s60次)不断递归调用此渲染函数,为了让render不断渲染最新画面
    requestAnimationFrame(animate);

    // 轨道控制器属性改变,需要跟新轨道控制器才有效果
    controls.update();

    // 3. 把场景+摄像机,传递给渲染器,渲染
    renderer.render(scene, camera);
}

createAxes();
createOrbit()
animate();

image-20241005070652871

适配场景大小

projection /prəˈdʒekʃən/ noun 预测;〔对过去的〕估算;

〔幻灯片的〕投射,投影;〔电影的〕放映;

aspect /ˈæspekt/ noun one part of a situation, idea, plan etc that has many parts 方面;

the direction in which a window, room, front of a building etc faces 朝向,方位;

the appearance of someone or something 外表,外观

the form of a verb in grammar that shows whether an action is continuing, or happens always, again and again, or once 〔动词的〕体〔表示动作正在进行、总是发生、反复发生或发生一次〕

matrix /ˈmeɪtrɪks/ noun 〔数学的〕矩阵; 〔人或社会的〕发源地,摇篮

function resize() {
    window.addEventListener('resize', () => {
        // 设置画布(世界黑色背景)的宽高
        renderer.setSize(window.innerWidth, window.innerHeight);
        // 摄像机宽高比(防止被压缩)
        camera.aspect = window.innerWidth / window.innerHeight;
        // 更新摄像机矩阵空间(让摄像机重新计算每个像素点位置和大小)
        camera.updateProjectionMatrix();
    })
}

创建物体需要哪些条件:

几何图形

three.js 提供一些常用的几何图形(文档几何体分类中找到)

不规则:

方法1:自己编写数学公式计算像素点位置(成本高 难)

方法2:找模型(建模师 / 网站提供的模型 3D 物体)

材质(皮肤),决定创建一个什么样的物体

three.js 三个基本物体:

点,线,面(网格)

网格:三角形(三角形可以组成任意的图形)

移动/旋转/缩放 立方体

物体位移:父级坐标系相对位移

旋转和缩放:自身坐标系旋转和缩放

Object3-position

Vector3

function moveCube() {
    // 物体的移动
    // 1. 位移
    // geometry.translate(5, 0, 0);
    // cube.position 拿到的是 Vector3 三维向量对象(世界坐标系中 x, y, z坐标点位置)
    // cube.position.x = 5;
    cube.position.set(5, 0, 0);

    // 2. 旋转
    // cube.rotation 原地的值 Euler 欧拉角对象(描述的绕着 x,y,z轴旋转)
    // 中轴线的“正方向”来看物体,物体是绕着“自身”轴线逆时针旋转
    cube.rotation.x = Math.PI / 4;
    const axces = new THREE.AxesHelper(3)
    cube.add(axces);

    // 3. 缩放
    // 沿着“自身”轴线缩放
    // 注意:中心点不动,向某个轴两边拉伸/缩小
    cube.scale.z = 2;
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    // cube.rotation.x += 0.01; // 沿x轴旋转
    // cube.rotation.z += 0.01; // 沿z轴旋转
    // cube.scale.z += 0.01; // 沿z轴缩放
    renderer.render(scene, camera);
}

旋转:

image-20241011102509428

案例—五颜六色立方体

image-20241011104717056

// 创建立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// const material = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, opacity: 0.4}); // 把立方体变为透明的
const colorArr = ['red', 'green', 'blue', 'orange', 'pink', 'silver'];
const materialArr = colorArr.map(colorStr => {
    return new THREE.MeshBasicMaterial({color: colorStr});
})
const cube = new THREE.Mesh(geometry, materialArr);
scene.add(cube);

旋转的五颜六色立方体:

image-20241019081006373

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

let controls;  // 轨道控制器
// three.js三要素
// 1. 场景:放置物体的容器世界
const scene = new THREE.Scene();

// 2. 摄像机:类似人眼,决定看到多大范围空间
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

//3. 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// const material = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, opacity: 0.4}); // 把立方体变为透明的
const colorArr = ['red', 'green', 'blue', 'orange', 'pink', 'silver'];
const materialArr = colorArr.map(colorStr => {
    return new THREE.MeshBasicMaterial({color: colorStr});
})
const cube = new THREE.Mesh(geometry, materialArr);
scene.add(cube);

camera.position.z = 5;

// 创建坐标系
function createAxes() {
    const axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);
}

// 创建轨道控制器(随鼠标滚轮滚动缩放,按住左键或右键移动)
function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

// 适配场景大小
function resize() {
    window.addEventListener('resize', () => {
        // 设置画布(世界黑色背景)的宽高
        renderer.setSize(window.innerWidth, window.innerHeight);
        // 摄像机宽高比(防止被压缩)
        camera.aspect = window.innerWidth / window.innerHeight;
        // 更新摄像机矩阵空间(让摄像机重新计算每个像素点位置和大小)
        camera.updateProjectionMatrix()
    })
}

// 立方体的移动
function moveCube() {
    // 位移
    cube.position.set(5, 0, 0);

    // 旋转
    // 从中轴线的“正方向”来看物体,物体是绕“自身”轴线逆时针旋转
    cube.rotation.x = Math.PI / 4;
    
    // 创建立方体的坐标轴
    const axces = new THREE.AxesHelper(3);
    cube.add(axces);
    
    // 缩放
    // 沿着“自身”轴线缩放
    // 注意:中心点不动,向某个轴两边拉伸/缩小
    cube.scale.z = 2;
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();

    // 立方体旋转
    cube.rotation.x += 0.01; // 沿着x轴旋转
    cube.rotation.y += 0.01; // 沿着y轴旋转
    cube.rotation.z += 0.01; // 沿着z轴旋转
    // cube.scale.z += 0.01; // 沿z轴缩放

    // 把场景+摄像机,传递给渲染器,渲染
    renderer.render(scene, camera);
}

createAxes();
createOrbit();
animate();
resize();
moveCube();

练习:创建球体Sphere

球缓冲几何体(SphereGeometry)

image-20241021110122806

image-20241030082051926

// 创建球体
function createSphere() {
    const geometry = new THREE.SphereGeometry(4, 32, 16);
    const material = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, opacity: 0.5});
    sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
}
createSphere();

多个立方体

// 创建多个立方体
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

let controls;
let cubeList = [];

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

camera.position.z = 10;

// 创建坐标系
function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}
createAxes();

// 创建轨道控制器(控制摄像机位置)
function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}
createOrbit();

// 窗口大小自适应
function resize() {
    window.addEventListener('resize', ()=> {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}
resize();

// 创建多个立方体
function createMultiCubes(cubeNum) {
    let cubeDataList = [];
    for (let i=0; i<cubeNum; i++) {
        cubeDataList.push({
            col: `rgb(${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)})`,
            width: Math.random() * 2 + 1, // 生成一个 1~3 之间的随机数
            height: Math.random() * 2 + 1,
            deep: Math.random() * 2 + 1,
            x: Math.random() * (5 - -5) + (-5),// 生成一个 -5~5 之间的随机数
            y: Math.random() * (5 - -5) + (-5),
            z: Math.random() * (5 - -5) + (-5),
        })
    }
    // console.log(cubeList);

    // 创建立方体,并加入到场景中
    cubeDataList.forEach(({col, width, height, deep, x, y, z}) => {
        let geometry = new THREE.BoxGeometry(width, height, deep);
        let material = new THREE.MeshBasicMaterial({color: col});
        let cube = new THREE.Mesh(geometry, material);
        cubeList.push(cube);
        cube.position.set(x, y, z);
        scene.add(cube);
    })
}
createMultiCubes(5);

// 循环渲染
function animate() {
    requestAnimationFrame(animate);
    controls.update();

    cubeList.forEach(cube => {
        cube.rotation.x += Math.random() * 0.01;
        cube.rotation.y += Math.random() * 0.01;
        cube.rotation.z += Math.random() * 0.01;
    })

    renderer.render(scene, camera);
}
animate();

性能监视器 —— stats

image-20241023071331764

stats /stæts/ noun [plural] informal statistics 统计数字[资料];统计学

// 目标:加入性能监视器,辅助我们调试3D项目
// 1. 引入stats附加组件
import Stats from 'three/addons/libs/stats.module.js';

let controls, cubeList = [], sta;

// 2. 创建性能监视器,并添加到页面
function createStats() {
    sta = new Stats();
    sta.setMode(0); // 0 每秒刷新帧数;1 每帧用时;2 内存占用
    sta.domElement.style.position = 'fixed';
    sta.domElement.style.left = '0';
    sta.domElement.style.top = '0';
    document.body.appendChild(sta.domElement);
}
createStats();

// 循环渲染
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    // 3. 不断更新当下最新状态数值
    sta.update();

    cubeList.forEach(cube => {
        cube.rotation.x += Math.random() * 0.01;
        cube.rotation.y += Math.random() * 0.01;
        cube.rotation.z += Math.random() * 0.01;
    })

    renderer.render(scene, camera);
}
animate();

image-20241023073938098

// 创建多个立方体

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
// 目标:加入性能监视器,辅助我们调试3D项目
// 1. 引入stats附加组件
import Stats from 'three/addons/libs/stats.module.js';

let controls, cubeList = [], sta;

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

camera.position.z = 10;

// 创建坐标系
function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}
createAxes();

// 创建轨道控制器(控制摄像机位置)
function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}
createOrbit();

// 窗口大小自适应
function resize() {
    window.addEventListener('resize', ()=> {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}
resize();

// 创建多个立方体
function createMultiCubes(cubeNum) {
    let cubeDataList = [];
    for (let i=0; i<cubeNum; i++) {
        cubeDataList.push({
            col: `rgb(${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)})`,
            width: Math.random() * 2 + 1, // 生成一个 1~3 之间的随机数
            height: Math.random() * 2 + 1,
            deep: Math.random() * 2 + 1,
            x: Math.random() * (5 - -5) + (-5),// 生成一个 -5~5 之间的随机数
            y: Math.random() * (5 - -5) + (-5),
            z: Math.random() * (5 - -5) + (-5),
        })
    }
    // console.log(cubeList);

    // 创建立方体,并加入到场景中
    cubeDataList.forEach(({col, width, height, deep, x, y, z}) => {
        let geometry = new THREE.BoxGeometry(width, height, deep);
        let material = new THREE.MeshBasicMaterial({color: col});
        let cube = new THREE.Mesh(geometry, material);
        cubeList.push(cube);
        cube.position.set(x, y, z);
        scene.add(cube);
    })
}
createMultiCubes(10);

// 2. 创建性能监视器,并添加到页面
function createStats() {
    sta = new Stats();
    sta.setMode = 0; // 0 每秒刷新帧数;1 每帧用时;2 内存占用
    sta.domElement.style.position = 'fixed';
    sta.domElement.style.left = '0';
    sta.domElement.style.top = '0';
    document.body.appendChild(sta.domElement);
}
createStats();

// 循环渲染
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    // 3. 不断更新当下最新状态数值
    sta.update();

    cubeList.forEach(cube => {
        cube.rotation.x += Math.random() * 0.01;
        cube.rotation.y += Math.random() * 0.01;
        cube.rotation.z += Math.random() * 0.01;
    })

    renderer.render(scene, camera);
}
animate();

删除物体

// 双击屏幕,删除立方体,按照加入cubArr的顺序从头删除
function bindDoubleClick() {
    window.addEventListener('dblclick', () => {
        const cube = cubeArr[0];
        if (!cube) return;
        cube.geometry.dispose(); // 删除几何图形对象(释放内存)
        cube.material.dispose(); // 删除材质对象
        scene.remove(cube); // 删除物体本身
        cubeArr.shift(); // 删除数组里头部cube对象
    })
}

光线投射

ray /reɪ/ ●○○ noun [countable]

  1. a straight narrow beam of light from the sun or moon 〔太阳或月亮的〕光线;光束
  2. a beam of heat, electricity, or other form of energy 〔热、电或其他能源的〕射线

caster, castor /ˈkɑːstə $ ˈkæstər/ noun [countable]

  1. a small wheel fixed to the bottom of a piece of furniture so that it can move in any direction 〔家具的〕小脚轮,滚轮
  2. British English a small container with holes in the top, used to spread sugar, salt etc on food 〔顶端有小孔,用来撒糖、盐等的〕调味瓶

vector /ˈvektə $ -ər/ noun [countable] technical

  1. a quantity such as force that has a direction as well as size 矢量,向量

intersect /ˌɪntəˈsekt $ -ər-/ verb

  1. [intransitive, transitive] if two lines or roads intersect, they meet or go across each other 〔两条线或道路〕相交,交叉
  2. [transitive] to divide an area with several lines, roads etc 〔用线、道路等把一个区域〕分隔

用鼠标与 3D 物体进行鼠标交互时

Raycaster

使用:屏幕坐标 归一化 WebGL 设备坐标

原因:因为 Raycaster 要去计算鼠标和那些物体有交互,需要走 WebGL 自己的坐标系数值,所以需要转换。

x 点坐标:(浏览器 x 轴坐标点 / 画布宽度) * 2 - 1

y 点坐标:- (浏览器 y 轴坐标点 / 画布高度) * 2 + 1

bug:3D 空间物体和 JS 数据一致

image-20241025063211533

image-20241025063342701

// 双击立方体,删除该立方体(利用光线投射)
function disposeCube() {
    // 1. 光线投射对象
    const raycaster = new THREE.Raycaster();

    // 2. 坐标转换
    const pointer = new THREE.Vector2();

    window.addEventListener('dblclick', (e)=>{
        pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
        pointer.y = - (e.clientY / window.innerHeight) * 2 + 1;

        // 3. 更新射线
        raycaster.setFromCamera(pointer, camera);

        // 4. 收集与射线交叉的物体
        // 参数:要让摄像机到鼠标点传过去一条线,与哪些3D物体交互计算
        // intersets 收集到你点过的物体们(数组)—— 顺序是按照由*到远
        // 使用cubeArr,有时候会报错,比如两个物体交叉时。
        console.log(0, cubeArr);
        const intersects = raycaster.intersectObjects(cubeArr);

        console.log('1', intersects);
        // 获取由*到远,第一个投射的物体
        if (!intersects[0]) return;
        const cube = intersects[0].object;
        console.log(cube);

        // 删除立方体
        cube.geometry.dispose();
        cube.material.dispose();
        scene.remove(cube);
        cubeArr.shift();
    })
}

双击删除指定立方体

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer, controls, cubeArr=[], stats;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

// 创建坐标轴
function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

// 创建轨道控制器(控制摄像机位置)
function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

// 循环渲染
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();
    renderer.render(scene, camera);
}

// 页面自适应
function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

// 创建多个立方体
function createMultiCube(cubeNum) {
    let dataArr = [];
    for(let i=0; i<cubeNum; i++) {
        dataArr.push({
            col: `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`,
            width: Math.random() * 2 + 1,
            height: Math.random() * 2 + 1,
            deep: Math.random() * 2 + 1,
            x: Math.random() * (5 - -5) - 5,
            y: Math.random() * (5 - -5) - 5,
            z: Math.random() * (5 - -5) - 5,
        })
    }

    dataArr.forEach(({col, width, height, deep, x, y, z}, index)=>{
        const geometry = new THREE.BoxGeometry(width, height, deep);
        const material = new THREE.MeshBasicMaterial({color: col})
        const cube = new THREE.Mesh(geometry, material);
        cube.position.set(x, y, z);
        scene.add(cube);
        
        // userData是3D物体的自定义属性
        cube.userData.ind = index;

        cubeArr.push(cube);
    })

    console.log(cubeArr);
}

// 双击对应立方体,删除
// 创建光线投射,定位到要删除的立方体的在渲染器的位置,删除资源,删除立方体
function delCube() {
    // 1. 创建射线
    const raycaster = new THREE.Raycaster();

    // 2. 转换坐标
    const pointer = new THREE.Vector2();
    window.addEventListener('dblclick', (e)=>{
        pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
        pointer.y = - (e.clientY / window.innerHeight) * 2 + 1;

        // 3. 更新射线
        raycaster.setFromCamera(pointer, camera);

        // 4. 收集与射线交叉的物体
        const intersects = raycaster.intersectObjects(cubeArr);
        if (!intersects[0]) return;

        // 删除物体
        const cube = intersects[0].object;
        cube.geometry.dispose();
        cube.material.dispose();
        scene.remove(cube);
        cubeArr.splice(cube.userData.ind, 1); // 删除立方体数组里对应索引的立方体
    })
}

// 创建性能监视器
function createStats() {
    stats = new Stats();
    stats.setMode(0); // 0 每秒几帧;1 每帧几秒;2 内存占用
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

init();
createAxes();
createOrbit();
resize();
createMultiCube(5);
delCube();
createStats();
animate();

标准网格材质、光源

目标:使用球体+标准网格材质,来对光做出反应。

image-20241030135006658

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer, controls, stats;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();
    renderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    stats = new Stats();
    stats.setMode(0);
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

// 目标:使用球体+标准网格材质,来对光做出反应
function createSphere() {
    // 1. 准备球体
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    // 注意:非基础材质都受到光照影响,表现的颜色需要和光照的颜色+强度进行综合计算
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    const sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
}

// 创建光源
function createLight() {
    // 2. 光源(物体) - *行光
    // 参数1: 光的颜色(白色),参数2:光源强度(值越大越亮,默认1)
    const directionalLight = new THREE.DirectionalLight(0xffffff, 3);
    directionalLight.position.set(3, 3, 3);
    scene.add(directionalLight);

    // 光源辅助对象(用线段来模拟光源位置)
    // 参数1:光源对象(给哪个光源创建辅助线);参数2:辅助线的宽高
    const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 2);
    scene.add(directionalLightHelper);
}

init();
createAxes();
createOrbit();
resize();
createStats();
createSphere();
createLight();
animate();

directional /dəˈrekʃənəl, daɪ-/ adjective

  1. relating to the direction in which something is pointing or moving 方向的
  2. a directional piece of equipment receives or gives out radio signals from some directions more strongly than others 定向的〔定向仪器接收或发出的无线电信号在某些方向上比其他方向强〕

mesh /meʃ/ 1. noun
1  [countable, uncountable] material made from threads or wires that have been woven together like a net, or a piece of this material 〔用线或金属丝编织的〕网状(织)物;网线

2  [countable usually singular] literary a complicated or difficult situation or system 错综复杂的局面;罗网

mesh 2. verb [intransitive]
1  if two ideas or things mesh, they fit together very well 〔两个想法或事物〕相合,相配,相互协调
mesh with

  1. if two parts of an engine or machine mesh, they fit closely together and connect with each other 〔引擎或机器部件〕啮合

创建地面、环境光(AmbientLight)

ambient /ˈæmbiənt/ adjective

  1. ambient temperature/light etc technical the temperature etc of the surrounding area 环境温度/光线等
  2. ambient music/sounds a type of modern music or sound that is slow, peaceful, and does not have a formal structure 背景音乐/音效
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer, controls, stats;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();
    renderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    stats = new Stats();
    stats.setMode(0);
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

function createSphere() {
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    const sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
}

// 目标:添加地面,了解环境光
function createFloor() {
    // 1. 准备*面
    const geometry = new THREE.PlaneGeometry(5, 5);
    // side: THREE.DoubleSide 决定物体哪一面渲染
    const material = new THREE.MeshStandardMaterial({color: 0xffff00, side: THREE.DoubleSide});
    const plane = new THREE.Mesh(geometry, material);
    plane.rotation.x = - Math.PI / 2;
    plane.position.y = -1;
    scene.add(plane);
}

function createLight() {
    // 2. 环境光(基础光源)
    // 特点1:照亮所有物体所有面
    // 特点2:没有位置,没有方向,没有阴暗面,没有光斑
    // 注意:three.js@0.155.0版本开始往后,光源照射的效果做了重构
    const light = new THREE.AmbientLight(0xfffff, 1);
    scene.add(light);
}

init();
createAxes();
createOrbit();
resize();
createStats();
createFloor();
createSphere();
createLight();
animate();

光源——点光源(PointLight)

image-20241031154635376

function createLight() {
    // 点光源(灯泡)
    // 特点:有位置,有方向,会阴暗面,会光斑
    // 参数1:光源颜色;参数2:光照强度;参数3:光最远距离
    const light = new THREE.PointLight(0xffffff, 6, 100);
    light.position.set(3, 3, 3);
    scene.add(light);

    // 点光源辅助对象
    const lightHelper = new THREE.PointLightHelper(light, 1);
    scene.add(lightHelper);
}

光源——*行光(DirectionalLight)

image-20241031160304976

function createLight() {
    // 目标:*行光(模拟太阳光)
    // 疑惑:太阳不是一个点吗?因为我们离太阳很远,光线过来后无限趋*于*行的
    // 特点:有方向,有位置,有阴暗面,有光斑
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(3, 3, 3);
    scene.add(light);

    // *行光辅助对象
    const lightHelper = new THREE.DirectionalLightHelper(light, 1);
    scene.add(lightHelper);
}

光源——聚光灯(SpotLight)

光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大。

构造器(Constructor)
SpotLight( color : Color, intensity : Float, distance : Float, angle : Radians, penumbra : Float, decay : Float )

color -(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色(0xffffff)的 Color 对象。
intensity -(可选)光照强度。默认值为 1。
distance - 光源照射的最大距离。默认值为 0(无限远)。
angle - 光线照射范围的角度。默认值为 Math.PI/3。
penumbra - 聚光锥的半影衰减百分比。默认值为 0。 光锥班周围的虚的程度(0-1,默认0,不虚)
decay - 沿着光照距离的衰减量。默认值为 2。

penumbra /pəˈnʌmbrə/ noun [countable]
technical an area of slight darkness 半阴影,半影

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer, controls, stats, sphere, lightHelper;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

let speed = 0.01;
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();

    sphere.position.x += speed;
    if (sphere.position.x >= 5) {
        speed = -0.01;
    } else if(sphere.position.x <= 0){
        speed = 0.01;
    }

    // 不断更新聚光灯的辅助线
    lightHelper.update();
    renderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    stats = new Stats();
    stats.setMode(0);
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

function createSphere() {
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
}

// 目标:添加地面,了解环境光
function createFloor() {
    // 1. 准备*面
    const geometry = new THREE.PlaneGeometry(5, 5);
    // side: THREE.DoubleSide 决定物体哪一面渲染
    // roughness 粗糙度(0 不粗糙——反光; 1 粗糙——不反光,看不到光斑)
    const material = new THREE.MeshStandardMaterial({color: 0xffffff, side: THREE.DoubleSide, roughness: 0.5});
    const plane = new THREE.Mesh(geometry, material);
    plane.rotation.x = - Math.PI / 2;
    plane.position.y = -1;
    scene.add(plane);
}

// function createLight() {
//     // 2. 环境光(基础光源)
//     // 特点1:照亮所有物体所有面
//     // 特点2:没有位置,没有方向,没有阴暗面,没有光斑
//     // 注意:three.js@0.155.0版本开始往后,光源照射的效果做了重构
//     const light = new THREE.AmbientLight(0xfffff, 1);
//     scene.add(light);
// }

// function createLight() {
//     // 点光源(灯泡)
//     // 特点:有位置,有方向,会阴暗面,会光斑
//     // 参数1:光源颜色;参数2:光照强度;参数3:光最远距离
//     const light = new THREE.PointLight(0xffffff, 6, 100);
//     light.position.set(3, 3, 3);
//     scene.add(light);

//     // 点光源辅助对象
//     const lightHelper = new THREE.PointLightHelper(light, 1);
//     scene.add(lightHelper);
// }

// function createLight() {
//     // 目标:*行光(模拟太阳光)
//     // 疑惑:太阳不是一个点吗?因为我们离太阳很远,光线过来后无限趋*于*行的
//     // 特点:有方向,有位置,有阴暗面,有光斑
//     const light = new THREE.DirectionalLight(0xffffff, 1);
//     light.position.set(3, 3, 3);
//     scene.add(light);

//     // *行光辅助对象
//     const lightHelper = new THREE.DirectionalLightHelper(light, 1);
//     scene.add(lightHelper);
// }

function createLight() {
    // 目标:聚光灯(手电筒 / 舞台聚光灯)
    // 特点1:从一个点出发,以一个角度向目标方向打出光线
    // 特点2:有位置,有方向,有阴暗面,有光斑
    // 参数3:聚光灯能打到多远
    // 参数4:聚光灯发射的角度大小(默认Math.PI / 3,最大不要超过Math.PI/2)
    // 参数5:光锥班周围的虚的程度(0-1,默认0,不虚)
    // 参数6:光照衰减量随着距离越远,在这个值的基础上就越弱
    const light = new THREE.SpotLight(0xffffff, 10, 100, Math.PI / 6, 0.5, 1);
    light.position.set(3, 3, 3);

    // 聚光灯设置一个贴图(让灯打出颜色+图片)图案
    // TextureLoader() 加载图片=>纹理对象
    // 代码中资源路径,以public作为vite构建服务器的根路径
    light.map = new THREE.TextureLoader().load('/desert.jpg');

    // 让聚光灯照射球体(让光源始终照向物体中心方向)
    light.target = sphere;
    scene.add(light);

    // 聚光灯辅助线
    lightHelper = new THREE.SpotLightHelper(light);
    scene.add(lightHelper);
}

init();
createAxes();
createOrbit();
resize();
createStats();
createFloor();
createSphere();
createLight();
animate();

texture /ˈtekstʃə $ -ər/ ●●○ noun [countable, uncountable]
1  the way a surface or material feels when you touch it, especially how smooth or rough it is 〔尤指光滑或粗糙的〕手感,质感,质地
smooth/silky/rough etc texture
the smooth texture of silk
丝绸的光滑质感
a designer who experiments with different colours and textures
尝试不同色彩和材质的设计师
2  the way that a particular type of food feels in your mouth 口感
creamy/crunchy/meaty etc texture
This soup has a lovely creamy texture.
这汤喝起来有奶油的口感,挺不错的。
3  formal the way the different parts of a piece of writing, music, art etc are combined in order to produce a final effect 〔文章、音乐、艺术等作品的〕神韵
the rich texture of Shakespeare’s English
莎士比亚笔下英语的丰富神韵

image-20241031174930561

SpotLightHelper——Constructor

SpotLightHelper( light : SpotLight, color : Hex )

light -- The SpotLight to be visualized.

color -- (optional) if this is not the set the helper will take the color of the light.

灯光与阴影

image-20241031185359331

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer, controls, stats, sphere;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // 1. 渲染器开启阴影支持
    renderer.shadowMap.enabled = true;
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();
    renderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    stats = new Stats();
    stats.setMode(0);
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

function createSphere() {
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    sphere = new THREE.Mesh(geometry, material);

    // 3. 物体设置产生阴影
    sphere.castShadow = true;

    scene.add(sphere);
}

function createFloor() {
    const geometry = new THREE.PlaneGeometry(5, 5);
    const material = new THREE.MeshStandardMaterial({color: 0xffffff, side: THREE.DoubleSide, roughness: 0.5});
    const plane = new THREE.Mesh(geometry, material);
    plane.rotation.x = - Math.PI / 2;
    plane.position.y = -1;

    // 4.地面接收阴影
    plane.receiveShadow = true;

    scene.add(plane);
}

function createLight() {
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(3, 3, 3);
    scene.add(light);

    // 2. 光源设置产生阴影(计算)
    light.castShadow = true;

    const lightHelper = new THREE.DirectionalLightHelper(light, 1);
    scene.add(lightHelper);
}

init();
createAxes();
createOrbit();
resize();
createStats();
createFloor();
createSphere();
createLight();
animate();

cast1 /kɑːst $ kæst/ ●●○ W3 verb (past tense and past participle cast)

1  cast light on/onto something to provide new information about something, making it easier to understand 使了解某事;阐明[论述]某事

2  cast doubt(s) on something to make people feel less certain about something 使怀疑某事,使不确信某事

3  LIGHT AND SHADE 光和阴影 [transitive] literary to make light or a shadow appear somewhere 投射〔光或影〕

4  cast a shadow/cloud over something literary to make people feel less happy or hopeful about something 给某事蒙上阴影

5  LOOK 看 [transitive] literary to look quickly in a particular direction 看[瞅]一眼

6  cast an eye on/over something to examine or read something quickly in order to judge whether it is correct, good etc 迅速检查[浏览]某物

7  cast a vote/ballot to vote in an election 投票

three.js 3D渲染器

image-20241102104116842

image-20241102104302288

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
// 目标:把原生DOM转换成3D物体
// 1.引入CSS3D渲染器
// 只关注DOM并使用CSS的transform进行3D变换,可以和WebGL的canvas搭配使用
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

let scene, camera, renderer, controls, sphere, floor, stats, light, lightHelper, cssRenderer;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 10;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    document.body.appendChild(renderer.domElement);

    // 2. 初始化3D渲染器标签
    cssRenderer = new CSS3DRenderer();
    cssRenderer.setSize(window.innerWidth, window.innerHeight);
    // CSS3D渲染器就是设置好了css3d样式的div标签
    document.body.appendChild(cssRenderer.domElement);
    cssRenderer.domElement.style.position = 'fixed';
    cssRenderer.domElement.style.left = 0;
    cssRenderer.domElement.style.top = 0;
    // 问题:div挡住了canvas,轨道控制器是监听canvas标签上的鼠标交互(从而影响摄像机位置角度)现在失效了。
    // 解决:把div的pointerEvent设置为'none'
    cssRenderer.domElement.style.pointerEvents = 'none'; // 不触发鼠标交互
}

// 3. JS创建原生DOM,并转换为3D物体
function create3DDom() {
    // 注意:如果用p标签,p标签有默认的margin,会影响three.js 3d 物体偏移16个单位
    const dom = document.createElement('span');
    dom.innerHTML = '球体';
    dom.style.color = 'red';
    // 注意:此dom不必自己手动添加到body,而是依赖于转换后3D物体渲染(会被three.js加入到div容器中)

    // 转换成three.js 3D物体
    const dom3D = new CSS3DObject(dom);
    // 虽然没有材质和几何图形属性,但是可以使用three.js中postition, rotation, scale
    dom3D.position.set(0, 2, 0);
    // 重点:原生DOM在转换为3D物体时(px大小用作three.js中,单位——默认16个单位大)
    // 让它缩放为1个单位大小
    dom3D.scale.set(1/16, 1/16, 1/16);
    scene.add(dom3D);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbitControls() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();

    renderer.render(scene, camera);
    // 4. 用css3D渲染器把当下世界和摄像机传入渲染
    cssRenderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);

        cssRenderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createSphere() {
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    sphere = new THREE.Mesh(geometry, material);
    sphere.castShadow = true;
    scene.add(sphere);
}

function createLight() {
    light = new THREE.SpotLight(0xffffff, 10, 8, Math.PI/6, 1, 1);
    light.position.set(3, 3, 3);
    light.castShadow = true;
    light.target = sphere;
    scene.add(light);

    lightHelper = new THREE.SpotLightHelper(light);
    scene.add(lightHelper);
}

function createFloor() {
    const geometry = new THREE.PlaneGeometry(5, 5);
    const material = new THREE.MeshStandardMaterial({color: 0xffffff, side: THREE.DoubleSide});
    floor = new THREE.Mesh(geometry, material);
    floor.rotation.x = - Math.PI / 2;
    floor.position.y = -1;
    floor.receiveShadow = true;
    scene.add(floor);
}

function createStats() {
    stats = new Stats();
    stats.setMode(0);
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

init();
createAxes();
createOrbitControls();
resize();
createSphere();
createLight();
createFloor();
createStats();
create3DDom();
animate();

CSS2D渲染器

image-20241103105503238

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
// 目标:把原生DOM转成2D物体
// 与3D使用基本一致
// 1. 引入需要的构造函数
import {CSS2DRenderer, CSS2DObject} from 'three/addons/renderers/CSS2DRenderer.js';

let scene, camera, renderer, controls, sta, cssRenderer;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // 2. 初始化2D渲染器标签(容器)
    cssRenderer = new CSS2DRenderer();
    cssRenderer.setSize(window.innerWidth, window.innerHeight);
    cssRenderer.domElement.style.position = 'fixed';
    cssRenderer.domElement.style.left = 0;
    cssRenderer.domElement.style.top = 0;
    cssRenderer.domElement.style.pointerEvents = 'none';
    document.body.append(cssRenderer.domElement);
}

// 3. 准备DOM转成2D物体
function create2DDom() {
    const dom = document.createElement('span');
    dom.innerHTML = 'Jacey';
    dom.style.color = 'orange';
    dom.style.fontSize = '50px';

    // 原生DOM转换成three.js 2D物体
    // 区别1:2D物体只能使用three.js position位移属性
    // 区别2:2D物体它不会把px单位转换three.js单位大小(默认大小还是依赖px像素)
    const dom2D = new CSS2DObject(dom);
    dom2D.position.y = 2;
    scene.add(dom2D);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    sta.update();
    renderer.render(scene, camera);
    // 4. css渲染器单独渲染DOM
    cssRenderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        cssRenderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    sta = new Stats();
    sta.setMode(0);
    sta.domElement.style.position = 'fixed';
    sta.domElement.style.left = 0;
    sta.domElement.style.top = 0;
    document.body.appendChild(sta.domElement);
}

init();
createAxes();
createOrbit();
resize();
createStats();
create2DDom();
animate();

CSS3D物体与CSS2D物体区别

css3D物体:不始终面向摄像机,不会被遮挡,跟着缩放,原生DOM点击。

css2D物体:始终面向摄像机,不会被遮挡,不跟着缩放,原生DOM点击。

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
import {CSS3DRenderer, CSS3DObject} from 'three/addons/renderers/CSS3DRenderer.js';
import {CSS2DRenderer, CSS2DObject} from 'three/addons/renderers/CSS2DRenderer.js';

// 目标:CSS3D物体与CSS2D物体区别
// css3D物体:不始终面向摄像机,不会被遮挡,跟着缩放,原生DOM点击。
// css2D物体:时钟面向摄像机,不会被遮挡,不跟着缩放,原生DOM点击。
let scene, camera, renderer, controls, sta, sphere, css3dRenderer, css2dRenderer;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 10000);
    camera.position.z = 10;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    css3dRenderer = new CSS3DRenderer();
    css3dRenderer.setSize(window.innerWidth, window.innerHeight);
    css3dRenderer.domElement.style.position = 'fixed';
    css3dRenderer.domElement.style.top = 0;
    css3dRenderer.domElement.style.left = 0;
    css3dRenderer.domElement.style.pointerEvents = 'none';
    document.body.appendChild(css3dRenderer.domElement);

    css2dRenderer = new CSS2DRenderer();
    css2dRenderer.setSize(window.innerWidth, window.innerHeight);
    css2dRenderer.domElement.style.position = 'fixed';
    css2dRenderer.domElement.style.top = 0;
    css2dRenderer.domElement.style.left = 0;
    css2dRenderer.domElement.style.pointerEvents = 'none';
    document.body.appendChild(css2dRenderer.domElement);

}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate)
    controls.update();
    sta.update();
    renderer.render(scene, camera);
    css3dRenderer.render(scene, camera);
    css2dRenderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        css3dRenderer.setSize(window.innerWidth, window.innerHeight);
        css2dRenderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    sta = new Stats();
    sta.setMode(0);
    sta.domElement.style.position = 'fixed';
    sta.domElement.style.left = 0;
    sta.domElement.style.top = 0;
    document.body.appendChild(sta.domElement);
}

function createSphere() {
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
}

function createFloor() {
    const geometry = new THREE.PlaneGeometry(5, 5);
    const material = new THREE.MeshStandardMaterial({color: 0xffffff, roughness: 0.5, side: THREE.DoubleSide});
    const floor = new THREE.Mesh(geometry, material);
    scene.add(floor);
    floor.rotation.x = - Math.PI / 2;
    floor.position.y = -1;
}

function createLight() {
    const light = new THREE.DirectionalLight(0xffffff, 5);
    scene.add(light);
    light.position.set(3, 3, 3);

    const lightHelper = new THREE.DirectionalLightHelper(light);
    scene.add(lightHelper);
}

function createDom() {
    const dom = document.createElement('span');
    dom.innerHTML = '球体';
    dom.style.color = 'pink';

    dom.addEventListener('click', ()=>{
        alert('hello,球体');
    })

    const dom3d = new CSS3DObject(dom);
    dom3d.position.y = 3;
    dom3d.scale.set(1/16, 1/16, 1/16);
    scene.add(dom3d);
}

function create2dDom() {
    const dom = document.createElement('span');
    dom.innerHTML = 'Shpere';
    dom.style.color = 'rgb(255, 255, 0)';
    dom.style.fontSize = '30px';

    // 问题:无法点击到2d物体。(原因:为了使用轨道控制器,将2d渲染器的鼠标监听设置为了none)
    // 但是可以点击到3d物体。(原因猜测:3d物体本质是存在于three.js的3D渲染器中的)。
    // 问题遗留,尚未解决
    dom.addEventListener('click', ()=>{
        alert('hello, shpere');
    })

    const dom2d = new CSS2DObject(dom);
    dom2d.position.set(3, 2, 2);
    scene.add(dom2d);
}

init();
createAxes();
createOrbit();
resize();
createStats();
createSphere();
createLight();
createFloor();
createDom();
create2dDom();
animate();

精灵物体

精灵是一个总是面朝着摄像机的*面,通常含有使用一个半透明的纹理。

精灵不会投射任何阴影,即使设置了castShadow = true也将不会有任何效果。

特点:加载图片转换成3D物体

始终面向摄像机,会被遮挡,放大缩小跟随,光线投射技术交互。

sprite /spraɪt/ noun [countable]
a fairy(1) 仙子,小精灵

image-20241105072248153

// 创建精灵物体
function createSprite() {
    // 精灵物体:加载半透明纹理图片,始终面向摄像机图片
    // TextureLoader纹理加载器
    // 作用:图片转换成纹理皮肤对象使用
    const map = new THREE.TextureLoader().load('/fish.png');
    // SpriteMaterial精灵物体特有材质(map: 颜色贴图)
    const material = new THREE.SpriteMaterial({map: map});
    // 创建精灵物体(而非网格物体)
    const sprite = new THREE.Sprite(material);
    sprite.position.set(-2, 0, 4);
    scene.add(sprite);
}

综合基础练习

练习1:

/**
 * 练习:场景、摄像机、渲染器,
 * 旋转的五颜六色立方体、多个立方体,
 * 利用光线投射双击删除立方体
 */

import * as THREE from 'three';
import Stats from 'three/addons/libs/stats.module.js';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

let scene, camera, renderer, sta, controls, colorfulCube, cubeArr=[];

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 10;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

// 创建性能监视器
function createStates() {
    sta = new Stats();
    sta.setMode(0);
    document.body.appendChild(sta.domElement);
}

// 创建坐标轴
function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

// 创建轨道控制器(控制摄像机位置)
function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
    // 阻尼效果(缓慢的停止转动摄像机)
    controls.enableDamping = true;
    // 阻尼的系数(缓慢的速度)
    controls.dampingFactor = 0.125;
}

// 窗口自适应
function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function animate() {
    requestAnimationFrame(animate);
    sta.update();
    controls.update();
    colorfulCube.rotation.x += 0.01;
    colorfulCube.rotation.y += 0.01;
    renderer.render(scene, camera);
}

// 创建五颜六色的立方体
function createColorfulCube() {
    const geometry = new THREE.BoxGeometry(1, 1, 1, 1);
    // const material = new THREE.MeshBasicMaterial({color: 0xffff00});
    const colorArr = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
    const material = colorArr.map(colorStr=> new THREE.MeshBasicMaterial({color: colorStr}))
    colorfulCube = new THREE.Mesh(geometry, material);
    colorfulCube.position.set(-3, 3, 0);
    scene.add(colorfulCube);
}

// 创建多个立方体
function createMultiCube(cubeNum) {
    let dataArr = []
    for(let i=0; i<cubeNum; i++) {
        dataArr.push({
            color: `rgb(${Math.floor(Math.random()*256)}, ${Math.floor(Math.random()*256)}, ${Math.floor(Math.random()*256)})`,
            width: Math.random() * 2 + 1, // 1~3
            height: Math.random() * 2 + 1,
            deep: Math.random() * 2 + 1,
            x: Math.random() * 10 - 5, // -5 ~ 5
            y: Math.random() * 10 - 5,
            z: Math.random() * 10 - 5,
        })
    }

    dataArr.forEach(({color, width, height, deep, x, y, z}, index)=>{
        const geometry = new THREE.BoxGeometry(width, height, deep);
        const material = new THREE.MeshBasicMaterial({color: color});
        const cube = new THREE.Mesh(geometry, material);
        cube.userData.index = index;
        cube.position.set(x, y, z);
        scene.add(cube);
        cubeArr.push(cube);
    })
}

// 利用光线投射,双击删除对应立方体
function delCube() {
    const raycaster = new THREE.Raycaster();
    const pointer = new THREE.Vector2();

    window.addEventListener('dblclick', (e)=>{
        pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
        pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;
        raycaster.setFromCamera(pointer, camera);
        let intersects = raycaster.intersectObjects(cubeArr);
        if(!intersects[0]) return;
        const cube = intersects[0].object;
        cube.geometry.dispose();
        cube.material.dispose();
        scene.remove(cube);
        cubeArr.slice(cube.userData.index, 1);
    })
}

init();
createStates();
createAxes();
createOrbit();
resize();
createColorfulCube();
createMultiCube(5);
delCube();
animate();

练习2:

/**
 * 练习:标准网格材质;光源;灯光与阴影;
 * 3D渲染器;2D渲染器;精灵物体
 */

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
import {CSS3DRenderer, CSS3DObject} from 'three/addons/renderers/CSS3DRenderer.js';
import {CSS2DRenderer, CSS2DObject} from 'three/addons/renderers/CSS2DRenderer.js';

let scene, camera, renderer, controls, sta, standardSphere, lightHelper, speed=0.01, css3dRenderer, css2dRenderer;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
    camera.position.z = 10;
    renderer = new THREE.WebGLRenderer();
    document.body.appendChild(renderer.domElement);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;

    css3dRenderer = new CSS3DRenderer();
    css3dRenderer.setSize(window.innerWidth, window.innerHeight);
    css3dRenderer.domElement.style.position = 'fixed';
    css3dRenderer.domElement.style.top = 0;
    css3dRenderer.domElement.style.left = 0;
    css3dRenderer.domElement.style.pointerEvents = 'none';
    document.body.appendChild(css3dRenderer.domElement);

    css2dRenderer = new CSS2DRenderer();
    css2dRenderer.setSize(window.innerWidth, window.innerHeight);
    css2dRenderer.domElement.style.position = 'fixed';
    css2dRenderer.domElement.style.top = 0;
    css2dRenderer.domElement.style.left = 0;
    css2dRenderer.domElement.style.pointerEvents = 'none';
    document.body.appendChild(css2dRenderer.domElement);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    sta.update();
    standardSphere.position.x += speed;
    if(standardSphere.position.x > 5) {
        speed = -0.01;
    }else if(standardSphere.position.x < -5) {
        speed = 0.01;
    }
    lightHelper.update();
    renderer.render(scene, camera);
    css3dRenderer.render(scene, camera);
    css2dRenderer.render(scene, camera);
}

function createStats() {
    sta = new Stats();
    sta.setMode(0);
    document.body.appendChild(sta.domElement);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        css3dRenderer.setSize(window.innerWidth, window.innerHeight);
        css2dRenderer.setSize(window.innerWidth, window.innerHeight);
    })
}

// 创建标准网格材质的球体
function createStandardSphere() {
    const geometry = new THREE.SphereGeometry(1, 32, 16);
    const material = new THREE.MeshStandardMaterial({color: 0xffff00});
    standardSphere = new THREE.Mesh(geometry, material);
    scene.add(standardSphere);
    standardSphere.castShadow = true;
}

// 创建环境光
function createAmbientLight() {
    const light = new THREE.AmbientLight(0xffffff, 5);
    scene.add(light);
}

// 创建地板
function createFloor() {
    const geometry = new THREE.PlaneGeometry(10, 5);
    const material = new THREE.MeshStandardMaterial({color: 0xffffff, side: THREE.DoubleSide});
    const floor = new THREE.Mesh(geometry, material);
    floor.rotation.x = -Math.PI / 2;
    floor.position.y = -1;
    scene.add(floor);
    floor.receiveShadow = true;
}

// 创建*行光
function createDirectionalLight() {
    const light = new THREE.DirectionalLight(0xffffff, 5);
    scene.add(light);

    light.position.set(3, 3, 3);

    const lightHelper = new THREE.DirectionalLightHelper(light, 1);
    scene.add(lightHelper);
}

// 创建点光源
function createPointLight() {
    const light = new THREE.PointLight(0xffffff, 5, 10, 1);
    scene.add(light);
    light.position.set(-3, 3, 3);

    const lightHelper = new THREE.PointLightHelper(light, 1);
    scene.add(lightHelper);
}

// 创建聚光灯
function createSpotLight() {
    const light = new THREE.SpotLight(0xffffff, 10, 10, Math.PI/6, 0.5, 1);
    scene.add(light);
    light.position.set(3, 3, 3);

    light.map = new THREE.TextureLoader().load('/desert.jpg');
    light.target = standardSphere;
    light.castShadow = true;

    lightHelper = new THREE.SpotLightHelper(light);
    scene.add(lightHelper);
}

// 创建3D物体
function create3dDom() {
    const dom = document.createElement('span');
    dom.innerHTML = '球体';
    dom.style.color = 'red';
    dom.addEventListener('click', ()=>{
        alert('hello, 球体');
    })

    const dom3d = new CSS3DObject(dom);
    dom3d.scale.set(1/16, 1/16, 1/16);
    dom3d.position.set(1.5, 2, 0);
    scene.add(dom3d);
}

// 创建2d物体
function create2dDom() {
    const dom = document.createElement('span');
    dom.innerHTML = 'Sphere';
    dom.style.color =  'pink';
    dom.style.fontSize = '32px';
    dom.addEventListener('click', ()=>{
        alert('hello, Sphere');
    })

    const dom2d = new CSS2DObject(dom);
    dom2d.position.set(-1, 2, 0);
    scene.add(dom2d);
}

// 创建精灵物体
function createSprite() {
    const material = new THREE.SpriteMaterial();
    material.map = new THREE.TextureLoader().load('/fish.png');
    const sprite = new THREE.Sprite(material);
    sprite.position.set(3, 3, 2);
    scene.add(sprite);
}

init();
createAxes();
createOrbit();
createStats();
resize();
createStandardSphere();
// createAmbientLight();
createFloor();
// createDirectionalLight();
// createPointLight();
createSpotLight();
create3dDom();
create2dDom();
createSprite();
animate();

项目

spline —— 建模师使用的建模软件

如果运行卡顿,Run Test 运行测试一下。让建模师想办法优化一下。

image-20241115080810012

image-20241115081058957

想不卡顿,就导出为灰模(物体还在,材质共用)

image-20241115081718657

threejs.org/editor 跟spline一样,都可以在线编辑模型。

image-20241115082300131

01. 素材项目准备

目标:在之前智慧社区的基础上,把模型重新使用three.js加载重构,让数据与中台项目联动。

image-20241115085021629

image-20241115083228502

安装环境 npm i

启动项目 npm run dev

  1. 把原来用spline加载模型代码注释掉

  2. 新建 src/three3d/index.js - 自己3d代码的入口,定义 init3d函数导出

    import * as THREE from 'three'
    
    // 导出初始化3d场景和内容的函数
    export function init3d() {
        console.log('init3d');
    }
    
  3. 在 big-index.vue 中使用

    import {init3d} from '@/three3d/index'
    
    onMounted(async() => {
    	// 保证图表依赖的数据已经完全返回,再做图表的初始化
        await getUserInfo()
        initBarChart()
        initPieChart()
        setTimeout(() => {
            init3d()
        }, 500)
    })
    
  4. 修改标签,canvas改成div并设置样式

    div 作为一会儿 three.js 画布的容器

<div class='model-container'>
    <!--
	模型渲染完毕之前显示loading,完毕之后显示3d模型 - 条件渲染 v-if
	-->
    <LoadingComponent :loading="isShowLoading" />
    <!-- 准备3D渲染节点 -->
    <div class="canvas-3d" ref="ref3d"></div>
</div>

.canvas-3d {
	width: 100%;
	height: 100%;
}
  1. 把素材文件夹three3d放入assets下

02. 3D环境准备

目标:准备three.js三要素和带颜色的背景,以及其他的辅助工具

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer;
let stats = new Stats();
stats.setMode(0);

function init3d() {
    scene = new THREE.Scene();
    // 设置场景世界的背景色
    scene.background = new THREE.Color(0x0f151e);
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(10, 10, 10);
    // antialias: true 抗锯齿处理(让渲染的图形边更光滑)
    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    document.body.appendChild(stats.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    // 阻尼效果(缓慢的停止转动摄像机)
    controls.enableDamping = true;
    // 阻尼的系数(缓慢的速度)
    controls.dampingFactor = 0.125;

    function renderLoop() {
        renderer.render(scene, camera);
        stats.update();
        requestAnimationFrame(renderLoop);
    }
    renderLoop();
}

init3d();

alias1 /ˈeɪliəs/ preposition
used when giving someone’s real name, especially an actor’s or a criminal’s name, together with another name they use 又名,化名〔尤指演员或罪犯〕
‘Friends’ star Jennifer Aniston, alias Rachel Green
《六人行》的主角珍妮佛·安尼斯顿,即瑞秋·格林

alias2 noun [countable]
a false name, usually used by a criminal 〔通常指罪犯用的〕假名,化名
a spy operating under the alias Barsad
一个化名巴萨德进行活动的间谍

03. 模型加载

GLTF加载器(GLTFLoader)

加载社区的模型

  1. 借助three.js提供的GLFLoader加载器可以加载模型数据对象到代码中

  2. 注意:

    • 加载的模型文件要放在服务器的根目录(但是这里是微前端,父子项目,需要把模型文件放到父项目public文件夹下,会放在服务器根目录下作为静态资源使用)

      loader.load('/mo.glb')
      
    • 我们放在子项目,还可以用import方式相对引入即可,但是需要去vue.config.js设置下支持glb文件的引入

      把glb文件作为静态资源直接复制到服务器根目录下

      import { defineConfig } from 'vite';
      
      export default defineConfig({
          // 把import引入glb文件的语句,当做静态资源进行复制,运行到服务器时,不对此文件做vite相关的翻译
          assetsInclude: ['**/*.glb']
      })
      
  3. 基于GLFLoader加载模型到scene场景中,在轨道控制器之后,渲染循环之前调用loadModel

    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // 模型加载
    import MoGLB from '@/assets/three3d/mo.glb'
      
    function loadModel() {
    	// 加载模型
        const loader = new GLTFLoader();
     	// 加载引入的模型文件,得到模型的对象   
        loader.load(MoGLB, (obj)=>{
            // 把模型加入到世界中(只有GLB文件对象加载的模型,在scene属性中)
            scene.add(obj.scene)
        })
    }
    
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import LTGLB from './assets/three3d/collision-world.glb';


let scene, camera, renderer;
let stats = new Stats();
stats.setMode(0);

function init3d() {
    scene = new THREE.Scene();
    // 设置场景世界的背景色
    scene.background = new THREE.Color(0x0f151e);
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(10, 10, 10);
    // antialias: true 抗锯齿处理(让渲染的图形边更光滑)
    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    document.body.appendChild(stats.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    // 阻尼效果(缓慢的停止转动摄像机)
    controls.enableDamping = true;
    // 阻尼的系数(缓慢的速度)
    controls.dampingFactor = 0.125;

    const loader = new GLTFLoader();
    loader.load(LTGLB, (obj)=>{
        scene.add(obj.scene);
    })

    function renderLoop() {
        renderer.render(scene, camera);
        stats.update();
        requestAnimationFrame(renderLoop);
    }
    renderLoop();
}

function createLight() {
    const light = new THREE.AmbientLight(0xffffff, 1);
    scene.add(light);
}

init3d();
createLight();

04. 环境光和着色器使用

  1. 添加光源

    function addLight() {
        // *行光
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
        directionalLight.position.set(10, 10, 10);
        scene.add(directionalLight);
    }
    
  2. 基于着色器修改模型材质

    loader.load(MoGLB, (obj)=>{
        scene.add(obj.scene)
        
     	// 渐变材质效果,因为模型本身原因,所有物体共用同一个材质
        const target = obj.scene.getObjectByName('楼顶')
        modifyCityDefaultMaterial(target);
    })
    

05. 小花坛和园区地面

目标:修改地面纯颜色,给小花坛物体表面贴图(纹理-皮肤)

  1. 图片用import方式引入

    import Crass01 from '@/assets/three3d/grass_01.jpg'
    
  2. 把图片加载成纹理对象并应用到小花坛表面

    // 小花坛
    const grass2 = obj.scene.getObjectByName('花坛07')
    // 遍历物体所有子物体
    grass2.traverse((child) => {
        // 图片——》纹理对象(皮肤)
        const texture = new THREE.TextureLoader().load(Grass01);
        // 颜色通道
        texture.colorSpace = THREE.SRGBColorSpace
        // 贴图在水*和垂直方向不断重复使用
        texture.wrapS = THREE.RepeatWrapping
        texture.wrapT = THREE.RepeatWrapping
        // 颜色贴图
        child.material = new THREE.MeshBasicMaterial({map: texture});
    })
    

    SRGBColorSpace:默认是linear,所以物体贴图默认是50%的时候显示的浅,而设置,sRGB就是正常的人感知的色彩空间

    image-20241118083808994

  3. 园区地面设置

    // 小花坛水池
    // 通过名字获取某个物体对象中的子物体
    const grass3 = obj.scene.getObjectByName('0bj123')
    grass3.material = new THREE.MeshBasicMaterial({color: new THREE.Color(0x31A5FF)})
    

    小花坛水池补充

    // 小花坛水池
    // 通过名字获取某个物体对象中的子物体
    const grass3 = obj.scene.getObjectByName('Obj321');
    grass3.material = new THREE.MeshBasicMaterial({color: new THREE.Color(0x31A5FF)})
    

traverse1 /ˈtrævɜːs $ trəˈvɜːrs/ verb [transitive]
formal to move across, over, or through something, especially an area of land or water 跨过,穿过,横越,横穿
two minutes to traverse the park
两分钟横穿公园
Related topics: Other sports
traverse2 /ˈtrævɜːs $ -vɜːrs/ noun [countable]
technical a sideways movement across a very steep slope in mountain-climbing 〔登山运动中爬陡坡时的〕横过山坡

06. 大花坛

// 大花坛
cons grass = obj.scenegetObjectByName('花坛14')
grass.traverse((child) => {
    const texture = new THREE.TextureLoader().load(Grass01)
    texture.colorSpace = THREE.SRGBColorSpace
    texture.wraps = THREE.RepeatWrapping
    texture.wrapT = THREE.RepeatWrapping
    child.material = new THREE.MeshBasicMaterial({map: texture})
})

07. 办公楼-房顶颜色

目标:设置每个房顶不同状态颜色

image-20241119075607554

如果你发现:你要操作的多个物体,有相同特性

相同特性:3D物体(名字),表格 tr(数据)

核心思想:数据 =》 视图

变的 -》 改数据 -》视图

function setHouseData(model) {
	// 办公楼数据
    let houseArr = [
        {
            id: '1',
            buildName: '办公楼1栋',
            roof: '房顶_7'
        },
        {
            id: '2',
            buildName: '办公楼2栋',
            roof: '房顶_12'
        },
        {
            id: '3',
            buildName: '办公楼3栋',
            roof: '房顶_6'
        },
        {
            id: '4',
            buildName: '办公楼4栋',
            roof: '房顶_13'
        },
        {
            id: '5',
            buildName: '办公楼14栋',
            roof: ['房顶_1', '房顶_2', '房顶_3']
        },
        {
            id: '6',
            buildName: '办公楼19栋',
            roof: ['房顶_28', '房顶_29']
        },
    ]
    
    // 遍历每个数据对象 =》通过对象中办公楼棚顶名字字符串 =》从大模型中找到办公楼棚顶小物体,进行相关设置
    // 默认材质
    const houseTopMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(0x409EFF) })
    // 已出租材质
    const houseHireMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(0xff6600) })
    
    houseArr.forEach(dataObj => {
        service({
            url: `/park/statistics/building?id=${dataObj.id}`
        }).then(res => {
            const obj = res.data
            if (dataObj.roof instanceof Array) {
                dataObj.roof.forEach(fdname => {
                    const fd = model.getObjectByName(fdName)
                    // 未出租
                    if (obj.status === 0) {
                        fd.material = houseTopMaterial
                    } else if (obj.status === 1) {
                        // 已出租
                        fd.material = houseHireMaterial
                    }
                })
            } else {
                // 此办公楼只有一个房顶
                const fd = model.getObjectByName(dataObj.roof)
                if (obj.status === 0) {
                    fd.material = houseTopMaterial
                } else if (obj.status === 1) {
                    fd.material = houseHireMaterial
                }
            }            
        })
    })
}

08. 停车场线条

目标:设置停车场线条显示

image-20241119090854859

function setParkData(model) {
    // 停车场数组
    let parkArr = [
        {
            id: '1',
            name: '停车场1号',
            rect: 'Rectangle',
            isHire: true,
            line: ['线_1', '线_2']
        },
        {
            id: '2',
            name: '停车场2号',
            rect: 'Rectangel_1',
            isHire: false,
            line: ['线_4', '线_5']
        }
    ]
    
    // 设置停车场颜色
    // 修改停车场
    // 把每个停车场物体(矩形设置为透明的)让线段显示出来
    parkArr.forEach(dataObj => {
        service({
            url: `/parking/area/${dataObj.id}`
        }).then(res => {
            const parkingLot1 = model.getObjectByName(dataObj.name)
            
            const rectangle = parkingLot1.getObjectByName(dataObj.rect)
            // opacity: 物体的整体透明度,材质默认不支持透明,所以还需要设置 transparent: true 开启透明度支持
            rectangle.material = new THREE.MeshStandardMaterial({color: 0x00ff00, transparent: true, opacity: 0})
            rectangle.userData.parkName = dataObj.name // 后续点击事件,点中矩形蒙层物体,要通过名字,找到所属停车场父级物体,添加说明div用
        })
    })
    
	// WebGL: 默认的线段粗细为 1 个单位
	// 粗细:百度搜 three.js 设置粗线段(如果是模型的线段,建议找建模师调整一下/代码获取调整替换)
}

09. 办公楼—说明标签

给办公楼设置说明标签

image-20241120081436366

  1. 选择始终面向摄像机的CSS2D渲染器,并准备好渲染器标签等
  2. 复制素材里的infoDiv.js模块用于创建标签原生DOM的模板函数
  3. 在办公楼循环请求数据后,把原生DOM转成3D物体加到办公楼上作为说明标签
// 创建说明标签(默认隐藏,后续点击时出现/切换)
const div = createDiv(res.data)
const object2d = new CSS2DObject(div)
// object2d.visible = false
object2d.name = 'infoDiv'
office.add(object2d)
// 点击标签上隐藏
div.addEventListener('click', (e)=> {
    e.stopProgation()
    object2d.visible = false
})
  1. 发现缺少样式,在App.vue中添加,注意style上不要加scoped,因为CSS渲染器标签在外面

    .modal-container {
        position: absolute;
        background: url('@/assets/modal-bg.png') no-repeat 0 0 / cover;
        width: 300px;
        padding: 12px 14px 0;
    }
    .close-icon {
        width: 20px;
        height: 20px;
        position: absolute;
        top: 6px;
        right: 6px;
        cursor: pointer;
    }
    .extra {
        position: absolute;
        right: 12px;
        top: 42px;
        background: rgba(10, 26, 52, 0.6);
        border: 1px solid rgba(54, 135, 255, 0.7);
        border-radius: 1px;
        box-shadow: 0px 0px 5px 0.5px rgba(3, 251, 255, 0.65) inset;
        padding: 4px 12px;
        color: rgba(255, 255, 255, 0.8);
        font-size: 14px;
    }
    

10. 停车场—说明标签

目标:给停车场加入说明标签,说明停车场状态。

image-20241120083850838

// 停车场负数标记
const div = createParkDiv(res.data)
const object2d = new CSS2DObject(div)
// object2d.visible = false
object2d.name = 'parkDiv'
parkingLot1.add(object2d)
div.addEventListener('click', (e)=>{
    e.stopPropagation()
    object2d.visible = false
})

11. 点击显示—说明标签

目标:点击办公楼/停车场, 显示提前准备好的说明标签

// 点击事件绑定
function bindClick(model) {
    const raycaster = new THREE.Raycaster()
    const pointer = new THREE.Vector2()
    window.addEventListener('click', e=>{
        // 把屏幕坐标 =》 WebGL设备坐标
        // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是(-1 to +1)
        pointer.x = (e.clientX / window.innerWidth) * 2 - 1
        pointer.y = -(e.clientY / window.innerHeight) * 2 + 1
        
        // 更新摄像机和鼠标之间的连线(位置)
        raycaster.setFromCamera(pointer, camera)
        const list = raycaster.intersectObjects(scene.clildren)
        // 点到了任意一个物体
        if (list.length > 0) {
            // 取出离我最*的这个 3d 物体
            const clickBuild = list[0].object
            // 我真的点击到了房顶
            if(clickBuild.name.indexOf('房顶') > -1) {
                // 房顶上的自定义属性名字(办公楼x栋),通过名字从根上找到它所属的办公楼大物体
                // 办公楼大物体,找到子说明标签 2d 物体,显示
                // 找到办公楼物体
                const office = model.getObjectByName(clickBuild.userData.officeName)
                const dom2d = office.getObjectByName('infoDiv')
                dom2d.visible = true
            }
        }
    })
}

12. 鼠标滑过变色

function bindMove() {
	// 鼠标滑过变色
    // 鼠标滑过事件
    let lastBuild
    let lastMaterial
    window.addEventListener('mousemove', e=>{
        e.stopPropagation()
        
        // 定义光线投射对象
        const raycaster = new THREE.Raycaster()
        // 定义二维向量对象(保存转换后的*面 x, y 坐标值)
        const pointer = new THREE.Vector2()
        
        // 把屏幕坐标 =》 WebGL设备坐标
        // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
        pointer.x = (e.clientX / window.innerWidth) * 2 - 1
        pointer.y = -(e.clientY / window.innerHeight) * 2 + 1
        
        // 更新摄像机和鼠标之间的连线(位置)
        raycaster.setFromCamera(pointer, camera)
        // 获取这条线穿过了哪些物体,收集成一个数组
        const list = raycaster.intersectObject(scene.children)
        
        if (list.length > 0) {
            const obj = list[0].object
            if (obj == lastBuild) return
            // 如果触摸的物体不是刚刚的
            lastBuild && (lastBuild.material == lastMaterial) && (lastBuild = null)
            // 注意只有房顶和停车场需要设置,所以 last 物体和材质 代码放 if 和 else 里
            // 判断是房顶/停车场
            if (obj.name.indexOf('房顶') > -1) {
                // 新的设置其他颜色
                const fd = scene.getObjectByName(obj.name)
                lastMaterial = fd.material
                fd.material = new THREE.MeshStandardMaterial({color: new THREE.Color(0x00d3ff)})
                lastBuild = fd
            } else if (obj.name.indexOf('Rectangle') > -1) {
                // 新的设置其他颜色
                const tcc = scene.getObjectByName(obj.name)
                lastMaterial = tcc.material
                tcc.material = new THREE.MeshStandardMaterial({color: new THREE.Color(0x00ff00), opacity: 0.5, transparent: true})
                lastBuild = tcc
            }
        }
    })
}

基础代码

import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';

let scene, camera, renderer, controls, stats;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
}

function createAxes() {
    const axes = new THREE.AxesHelper(5);
    scene.add(axes);
}

function createOrbit() {
    controls = new OrbitControls(camera, renderer.domElement);
}

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    stats.update();
    renderer.render(scene, camera);
}

function resize() {
    window.addEventListener('resize', ()=>{
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    })
}

function createStats() {
    stats = new Stats();
    stats.setMode(0);
    stats.domElement.style.position = 'fixed';
    stats.domElement.style.left = 0;
    stats.domElement.style.top = 0;
    document.body.appendChild(stats.domElement);
}

init();
createAxes();
createOrbit();
resize();
createStats();
animate();

想练习的案例

案例1

http://localhost:8080/examples/#webgl_lights_hemisphere

image-20241121074922017

这段代码是一个使用three.js库创建的3D场景的HTML页面。它展示了如何使用半球光(HemisphereLight)和方向光(DirectionalLight)来照亮一个3D模型。下面是代码的逐部分解析:

  1. HTML结构

    • <!DOCTYPE html> 声明文档类型。
    • <html lang="en"> 定义了页面的语言为英语。
    • <head> 部分包含了页面的元数据,如字符集、视口设置、样式链接和内联样式。
  2. 样式

    • 内联样式定义了页面的基本颜色和链接颜色。
  3. 主体内容

    • <body> 标签内包含了页面的主要内容。
    • <div id="container"></div> 创建了一个容器,用于放置3D场景。
    • <div id="info"> 包含了一些信息链接,指向three.js官网和模型来源。
  4. 顶点着色器和片段着色器

    • 两个<script>标签定义了自定义的着色器,用于天空盒的渲染。
  5. 导入映射

    • <script type="importmap"> 定义了模块导入的路径。
  6. JavaScript模块

    • 使用<script type="module"> 引入了three.js库和其他辅助库,如Stats用于性能监控,GUI用于创建控制面板。
  7. 初始化和渲染逻辑

    • init() 函数初始化场景,包括相机、灯光、地面、天空盒和模型。
    • onWindowResize() 函数处理窗口大小变化时的相机和渲染器尺寸调整。
    • animate()render() 函数用于动画循环和渲染场景。
  8. 灯光

    • 创建了一个半球光和一个方向光,设置了它们的颜色、位置和强度。
    • 使用了灯光助手(LightHelper)来可视化灯光范围。
  9. 地面和天空盒

    • 创建了一个*面作为地面,并设置了材质。
    • 使用着色器创建了一个天空盒,模拟了天空的颜色渐变。
  10. 模型加载

    • 使用GLTFLoader加载了一个3D模型,并设置了模型的位置、缩放和动画。
  11. 渲染器设置

    • 设置了WebGLRenderer,启用了阴影映射,并添加了渲染器到页面中。
  12. 性能监控和控制面板

    • 添加了Stats.js来监控性能,并创建了一个GUI控制面板来控制灯光和阴影强度。
  13. 动画循环

    • animate()函数中,更新了动画混合器(AnimationMixer)的状态,并渲染了场景。

这段代码展示了three.js在3D场景中的多种用途,包括灯光、阴影、着色器和模型加载。如果你需要进一步的帮助或者有具体的问题,请随时提问。

这段代码中的3D模型是通过使用GLTFLoader加载的。GLTF(GL Transmission Format)是一种用于3D场景和模型的传输格式,它被设计为一种高效、可扩展的方式来传输3D内容。以下是代码中加载3D模型的具体步骤:

  1. 导入GLTFLoader

    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
    

    这行代码从three/addons/loaders目录中导入了GLTFLoader模块。

  2. 创建GLTFLoader实例

    const loader = new GLTFLoader();
    

    这行代码创建了一个新的GLTFLoader实例。

  3. 使用loader.load方法加载模型

    loader.load('models/gltf/Flamingo.glb', function (gltf) {
        // ...处理加载的模型...
    });
    

    loader.load方法接受两个参数:模型文件的路径和加载完成后的回调函数。在这个例子中,模型文件的路径是'models/gltf/Flamingo.glb',这是一个GLB格式的文件,GLB是GLTF的二进制版本。

  4. 在回调函数中处理加载的模型

    const mesh = gltf.scene.children[0];
    const s = 0.35;
    mesh.scale.set(s, s, s);
    mesh.position.y = 15;
    mesh.rotation.y = -1;
    
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    
    scene.add(mesh);
    
    • gltf.scene包含了加载的3D场景,gltf.scene.children是一个包含场景中所有子对象的数组。这里,我们假设模型是场景中的第一个子对象,所以使用gltf.scene.children[0]来获取模型。
    • mesh.scale.set(s, s, s)设置模型的缩放比例。
    • mesh.position.y = 15设置模型在Y轴上的位置。
    • mesh.rotation.y = -1设置模型的旋转角度。
    • mesh.castShadow = truemesh.receiveShadow = true分别设置模型是否投射阴影和接收阴影。
    • scene.add(mesh)将模型添加到场景中。
  5. 处理模型的动画

    const mixer = new THREE.AnimationMixer(mesh);
    mixer.clipAction(gltf.animations[0]).setDuration(1).play();
    mixers.push(mixer);
    
    • 如果模型包含动画,gltf.animations是一个包含所有动画的数组。这里,我们创建了一个AnimationMixer实例,并使用模型的第一个动画。
    • mixer.clipAction(gltf.animations[0]).setDuration(1).play()设置动画的持续时间并播放动画。
    • mixers.push(mixer)将动画混合器添加到mixers数组中,以便在动画循环中更新。

通过这些步骤,代码成功地加载了一个GLTF格式的3D模型,并将其添加到three.js场景中,同时处理了模型的缩放、位置、旋转和阴影。如果模型包含动画,还会播放动画。

案例2

http://localhost:8080/examples/#webgl_loader_collada_kinematics

image-20241121075042379

http://localhost:8080/examples/#webgl_loader_ifc

image-20241121080455879

http://localhost:8080/examples/#webgl_loader_collada_skinning

image-20241121075104271

http://localhost:8080/examples/#webgl_loader_fbx

image-20241121075127296

http://localhost:8080/examples/#webgl_loader_gltf_sheen

image-20241121075217711

http://localhost:8080/examples/#webgl_loader_vox

image-20241121075431675

http://localhost:8080/examples/#webgl_panorama_cube

image-20241121075646872

http://localhost:8080/examples/#webgl_rendertarget_texture2darray

image-20241121075810388

http://localhost:8080/examples/#webgpu_animation_retargeting

image-20241121075917367

http://localhost:8080/examples/#webgpu_tsl_earth

image-20241121080033907

http://localhost:8080/examples/#webgl_performance

image-20241121080213968

有哪些three.js的优秀初级练手项目?

对于想要开始使用three.js的初学者来说,有一些练手项目可以帮助你快速上手并理解three.js的核心概念。以下是一些适合初学者的项目:

  1. 基础3D场景创建:创建一个简单的3D场景,包括一个立方体、相机设置、光源设置和渲染器对象。这是学习three.js的基础,可以通过修改几何体、材质和光照来进一步探索。

  2. 3D模型加载:学习如何加载外部3D模型文件,如OBJ或GLTF格式,这可以通过使用three.js提供的加载器实现。

  3. 纹理和材质:创建一个简单的几何体,并学习如何应用不同的纹理和材质,以实现更加真实的视觉效果。

  4. 光照和阴影:通过添加不同的光源(如环境光、点光源、聚光灯)来学习光照的基础知识,并尝试实现阴影效果。

  5. 相机控制:实现一个可以交互的3D场景,通过键盘和鼠标控制相机的移动和旋转,可以使用OrbitControls来简化操作。

  6. 动画制作:创建一个简单的动画,例如让一个物体在场景中移动或旋转,这可以通过使用requestAnimationFrame或者three.js的动画系统来实现。

  7. 物理碰撞:尝试将物理碰撞的概念融入到3D场景中,例如创建一个可以反弹的小球。

  8. 3D避障游戏:这是一个更复杂的项目,可以创建一个3D避障游戏,玩家需要控制一个物体避开障碍物。

  9. 可视化大屏:基于vue3实现的一个可视化3D项目,包含了场景、灯光、摄像机初始化,模型、天空盒的加载,以及鼠标点击和悬浮的事件交互。

  10. 3D编辑器:尝试创建一个简单的3D编辑器,允许用户在网页上对3D模型进行简单的操作和编辑。

这些项目不仅可以帮助你理解three.js的基本概念,如场景、相机、渲染器、几何体、材质和光照,还可以让你通过实践来提高你的技能。随着你对three.js的熟悉,你可以尝试更复杂的项目,如创建交互式3D应用或者3D游戏。

github上有哪些three.js的优秀初级练手项目?

GitHub 上有许多优秀的 Three.js 初级练手项目,以下是一些推荐:

  1. 创意个人网站:这是一个使用 Three.js 创建的个人网站示例,通过汽车浏览作者的作品集和其他信息,融入了物理碰撞元素。

  2. 朋克风格3D数字大屏:这是一个超酷的赛博朋克风格的3D数字地球大屏,使用 React + Three.js + Echarts + stylus 技术栈。

  3. 智慧园区:基于 Vite + Vue3 + Three.js 实现的智慧园区,可以切换不同视角自动预览,点击楼层可三维切面展示。

  4. 炫酷个人3D网站:将个人形象、风格、作品等以更加立体、生动的方式呈现,基于物理引擎(Ammo.js)与3D渲染对象(Three.js)相结合。

  5. 海洋 - 逼真的水着色器:该项目提出了一个 Three.js 着色器的实现,以提供逼真的*面水效果。

  6. 室内3D地图:基于 Three.js 的 JavaScript 库,用于显示室内地图,支持 2D 和 3D 地图。

  7. 3D钢琴演奏家:使用 Three.js 和 MIDI.js 搭建的 3D 钢琴演奏家,可以切换不同的钢琴曲。

  8. 3D地球数据可视化:Gio.js 是一个基于 Three.js 的 web 3D 地球数据可视化的开源组件库。

  9. 可视化大屏:基于 vue3 实现的一个可视化 3D 项目,包含了场景、灯光、摄像机初始化,模型、天空盒的加载,以及鼠标点击和悬浮的事件交互。

  10. 3D避障游戏:这是一个在 Replit 中创建的游戏,可以在浏览器中完成本教程,并且可以轻松地在线共享你的游戏。

这些项目都是很好的练手资源,可以帮助初学者快速上手 Three.js 并理解其核心概念。

📚 有哪些three.js的学习资源推荐?

Three.js 是一个非常强大的 JavaScript 库,用于在 web 浏览器中创建和显示 3D 图形。以下是一些推荐的资源,可以帮助你学习 Three.js:

  1. Three.js 官方文档:提供了全面的 API 参考和入门指南,适合所有级别的开发者。

  2. Three.js 中文网:提供了 Three.js 的中文文档和教程,适合中文用户学习。

  3. Three.js Fundamentals:一个由社区驱动的项目,旨在提供详细的教程和指南。

  4. Discover three.js:一个系列教程,帮助你从基础到高级逐步学习 Three.js。

  5. Three.js 教程系列:CSDN 上的博客提供了详细的 Three.js 教程系列,适合初学者。

  6. Three.js 学习路线和资源:CSDN 上的另一篇文章,提供了学习路线、网站、资料和案例。

  7. Three.js 官方示例:官方提供的示例代码,可以直接在浏览器中运行和查看效果,适合实践学习。

  8. Stack Overflow:一个编程问答社区,你可以在这里搜索和提问有关 Three.js 的问题。

  9. GitHub:Three.js 的 GitHub 仓库,你可以在这里查看源代码,甚至贡献代码。

  10. Learn Three.js:一本关于 Three.js 的书籍,提供了详细的教程和实例。

这些资源涵盖了从基础到高级的 Three.js 知识,包括教程、文档、示例代码和社区支持。通过这些资源,你可以逐步提高你的 Three.js 技能。

posted @ 2024-12-14 22:03  JaceyKan  阅读(19)  评论(0编辑  收藏  举报