普通视图

发现新文章,点击刷新页面。
昨天以前首页

WebGL线段绘制:掌握三种线段图元类型

2026年2月12日 15:08

线段图元的三种类型

WebGL提供了三种不同的线段绘制模式,每种都有其独特的用途和特点:

1. 基本线段(LINES)

每条线段都需要明确指定两个端点,线段之间相互独立。

// 每两个点构成一条独立线段
// [v1, v2] 构成第一条线段
// [v3, v4] 构成第二条线段
gl.drawArrays(gl.LINES, 0, 4); // 绘制2条线段

特点: 每次需要2个顶点来绘制1条线段,线段之间不相连。

2. 带状线段(LINE_STRIP)

线段首尾相连,形成连续的线条。

// [v1, v2] 第一条线段
// [v2, v3] 第二条线段(使用前一线段的终点作为起点)
// [v3, v4] 第三条线段
gl.drawArrays(gl.LINE_STRIP, 0, 4); // 绘制3条相连的线段

特点: 除了第一条线段需要2个点,后续每个点都会与前一个点形成新线段。

3. 环状线段(LINE_LOOP)

在带状线段的基础上,自动连接最后一个点和第一个点。

// [v1, v2], [v2, v3], [v3, v4], [v4, v1] 形成闭合环
gl.drawArrays(gl.LINE_LOOP, 0, 4); // 绘制闭合的四边形边框

特点: 形成闭合的环状线条,非常适合绘制轮廓。

交互式线段绘制实现

让我们通过一个鼠标点击绘制线段的示例来理解这些概念:

screenshot_2026-02-12_15-07-11.gif

JavaScript交互代码

// 存储点击位置的数组
var positions = [];

// 监听鼠标点击事件
canvas.addEventListener('mouseup', function(e) {
    var x = e.offsetX;  // 获取相对于canvas的X坐标
    var y = e.offsetY;  // 获取相对于canvas的Y坐标
    
    // 将点击坐标添加到数组
    positions.push(x);
    positions.push(y);
    
    // 更新缓冲区数据
    gl.bufferData(
        gl.ARRAY_BUFFER,
        new Float32Array(positions),
        gl.DYNAMIC_DRAW
    );
    
    // 重新绘制
    render();
});

// 渲染函数
function render() {
    // 清空画布
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    // 根据需要选择不同的线段模式
    // 绘制基本线段(每次需要2个点绘制1条线)
    gl.drawArrays(gl.LINES, 0, positions.length / 2);
    
    // 或绘制带状线段(连续连接所有点)
    // gl.drawArrays(gl.LINE_STRIP, 0, positions.length / 2);
    
    // 或绘制环状线段(连接所有点并闭合)
    // gl.drawArrays(gl.LINE_LOOP, 0, positions.length / 2);
}

着色器程序

顶点着色器和片元着色器与之前三角形的例子类似,只是处理的顶点数据用于绘制线段:

// 顶点着色器
precision mediump float;
attribute vec2 a_Position;
attribute vec2 a_Screen_Size;

void main() {
    // 将屏幕坐标转换为WebGL坐标系统
    vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
    position.y = -position.y; // 翻转Y轴
    gl_Position = vec4(position, 0.0, 1.0);
}
// 片元着色器
precision mediump float;
uniform vec4 u_Color;

void main() {
    gl_FragColor = u_Color;
}

三种线段模式的实际应用

基本线段(LINES)适用场景

  • 绘制独立的直线段
  • 连接特定的点对
  • 路径规划中的独立路段

带状线段(LINE_STRIP)适用场景

  • 绘制连续的路径
  • 手写轨迹绘制
  • 曲线轮廓绘制

环状线段(LINE_LOOP)适用场景

  • 绘制封闭图形的边框
  • 凸多边形轮廓
  • 环形路径

性能考虑

// 使用STATIC_DRAW适用于不经常改变的数据
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

// 使用DYNAMIC_DRAW适用于经常改变的数据
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
  • STATIC_DRAW:数据只设置一次,多次使用
  • DYNAMIC_DRAW:数据多次更新,多次使用
  • STREAM_DRAW:数据少量修改,少量使用

实践建议

  1. LINES模式:每次点击两个点后才能看到一条线段,适合绘制独立的线段对
  2. LINE_STRIP模式:每次点击都会延续之前的线条,适合连续绘制
  3. LINE_LOOP模式:在LINE_STRIP基础上自动闭合,适合绘制封闭形状

掌握了这三种线段绘制模式,你就可以创建各种线条效果,从简单的几何图形到复杂的路径可视化都能轻松实现!

WebGL三角形绘制:掌握缓冲区与基本图元

2026年2月12日 14:56

三角形图元的三种类型

在WebGL中,三角形是最基本的图元之一,但你知道吗?三角形有三种不同的绘制模式,每种都有其独特用途:

1. 基本三角形(TRIANGLES)

这是最常用的三角形绘制方式。每3个顶点构成一个独立的三角形,互不干扰。

// 6个顶点绘制2个三角形
// [v1, v2, v3] 构成第一个三角形
// [v4, v5, v6] 构成第二个三角形
gl.drawArrays(gl.TRIANGLES, 0, 6); // 绘制2个三角形

绘制三角形数量 = 顶点数 ÷ 3

2. 三角带(TRIANGLE_STRIP)

相邻的三角形共享边,效率更高。

// 6个顶点可以绘制4个三角形
// [v1, v2, v3], [v3, v2, v4], [v3, v4, v5], [v5, v4, v6]
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 6); // 绘制4个三角形

绘制三角形数量 = 顶点数 - 2

3. 三角扇(TRIANGLE_FAN)

所有三角形都以第一个顶点为公共顶点。

// 适用于绘制扇形或圆形
gl.drawArrays(gl.TRIANGLE_FAN, 0, 6); // 绘制4个三角形

绘制三角形数量 = 顶点数 - 2

绘制固定三角形

让我们从最简单的固定三角形开始:

着色器程序

顶点着色器:

// 设置浮点数据类型为中级精度
precision mediump float;
// 接收顶点坐标 (x, y)
attribute vec2 a_Position;

void main(){
   gl_Position = vec4(a_Position, 0, 1);
}

片元着色器:

// 设置浮点数据类型为中级精度
precision mediump float;
// 接收 JavaScript 传过来的颜色值(rgba)
uniform vec4 u_Color;

void main(){
   vec4 color = u_Color / vec4(255, 255, 255, 1);
   gl_FragColor = color;
}

JavaScript核心代码

// 定义三角形的三个顶点(右下角、左上角、左下角)
var positions = [1, 0, 0, 1, 0, 0]; // 每两个数字代表一个顶点的x、y坐标

// 获取着色器变量位置
var a_Position = gl.getAttribLocation(program, 'a_Position');
var u_Color = gl.getUniformLocation(program, 'u_Color');

// 创建缓冲区
var buffer = gl.createBuffer();

// 绑定缓冲区为当前操作对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

// 将顶点数据写入缓冲区
// 注意:必须使用类型化数组(Float32Array)传递给WebGL
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

// 启用顶点属性
gl.enableVertexAttribArray(a_Position);

// 配置顶点属性如何从缓冲区读取数据
var size = 2;        // 每次读取2个数据(x, y)
var type = gl.FLOAT; // 数据类型为浮点型
var normalize = false; // 不需要标准化
var stride = 0;      // 步长为0,表示数据连续存放
var offset = 0;      // 从缓冲区开始位置读取
gl.vertexAttribPointer(a_Position, size, type, normalize, stride, offset);

// 设置颜色并绘制
gl.uniform4f(u_Color, 255, 0, 0, 255); // 设置为红色

// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3); // 绘制3个顶点,即1个三角形

缓冲区操作详解

WebGL中的缓冲区是向GPU传递数据的关键工具,以下是其工作流程:

1. 创建和绑定缓冲区

// 创建缓冲区对象
var buffer = gl.createBuffer();

// 将缓冲区绑定到ARRAY_BUFFER目标
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

2. 向缓冲区写入数据

// 使用类型化数组确保数据格式正确
var typedArray = new Float32Array([x1, y1, x2, y2, x3, y3]);
gl.bufferData(gl.ARRAY_BUFFER, typedArray, gl.STATIC_DRAW);

3. 配置顶点属性指针

gl.enableVertexAttribArray(attributeLocation);
gl.vertexAttribPointer(
    attributeLocation, // attribute变量位置
    size,              // 每个顶点包含的分量数(2=x,y; 3=x,y,z)
    type,              // 数据类型
    normalize,         // 是否标准化
    stride,            // 步长
    offset             // 偏移量
);

重要提示: WebGL要求强类型数据,JavaScript中的普通数组必须转换为类型化数组(如Float32Array)才能传递给GPU。

动态绘制三角形

现在让我们实现一个交互式功能:点击三次鼠标绘制一个三角形。

screen_recording_2026-02-12_14-54-03.gif

增强版顶点着色器

// 设置浮点数精度为中等精度
precision mediump float;
// 接收顶点坐标 (x, y)
attribute vec2 a_Position;
// 接收 canvas 的尺寸(width, height)
attribute vec2 a_Screen_Size;

void main(){
    // 将canvas坐标转换为NDC坐标(-1到1的范围)
    vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
    position = position * vec2(1.0, -1.0); // 翻转Y轴
    gl_Position = vec4(position, 0, 1);
}

交互式JavaScript实现

// 存储点击位置的数组
var positions = [];

// 绑定鼠标点击事件
canvas.addEventListener('mouseup', function(e) {
    var x = e.offsetX; // 相对于canvas的X坐标
    var y = e.offsetHeight - e.offsetY; // 转换Y坐标系统
    positions.push(x, y);
    
    // 当顶点数是6的倍数时(即3个点),绘制一个三角形
    if (positions.length % 6 === 0) {
        // 更新缓冲区数据
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
        
        // 重新绘制所有三角形
        redraw();
    }
});

function redraw() {
    // 清空画布
    gl.clearColor(0, 0, 0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    // 绘制所有三角形
    gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
}

核心概念总结

缓冲区操作流程

  1. gl.createBuffer() - 创建缓冲区对象
  2. gl.bindBuffer() - 绑定为当前操作缓冲区
  3. gl.bufferData() - 向缓冲区写入数据
  4. gl.enableVertexAttribArray() - 启用顶点属性
  5. gl.vertexAttribPointer() - 配置属性读取方式
  6. gl.drawArrays() - 执行绘制

类型化数组的重要性

  • JavaScript普通数组无法直接传递给WebGL
  • 必须使用Float32Array等类型化数组
  • 确保数据格式与GPU要求一致

坐标转换

  • Canvas坐标系:左上角(0,0),Y轴向下
  • WebGL坐标系:中心(0,0),Y轴向上
  • 需要进行坐标转换才能正确显示

掌握了三角形绘制和缓冲区操作,你就迈出了WebGL高级渲染的第一步!接下来可以尝试绘制更多有趣的图形。

🎨 Three.js 自定义材质贴图偏暗?一文搞懂颜色空间与手动 Gamma 矫正

2026年2月12日 12:20

在做智驾标注工具时踩了个坑:为什么我写的 Shader 贴图总是比原图暗?深入探究颜色空间的奥秘!


📌 问题引入:贴图怎么变暗了?

最近在开发一个基于 Three.js 的 3D 标注工具,需要自定义着色器来实现一些特殊的贴图效果。写了个最基础的贴图材质:

varying vec2 vUv;
uniform sampler2D texture;

void main() {
  vec4 texColor = texture2D(texture, vUv);
  gl_FragColor = texColor;
}

结果傻眼了:渲染出来的贴图比原图明显偏暗!

这不对啊!我明明就是直接采样贴图,怎么就变暗了?难道是纹理加载的问题?还是我的光照设置有问题?


🔍 问题根源:颜色空间的"暗箱操作"

经过一番排查,终于找到了罪魁祸首:颜色空间(Color Space)自动转换

🎯 Three.js 的默认行为

Three.js 为了保证物理正确的光照计算,默认会对纹理做这样的处理:

const texture = new THREE.TextureLoader().load('image.jpg');
// Three.js 自动设置:
texture.colorSpace = THREE.SRGBColorSpace; // r152+ 旧版用 encoding

这意味着:

  1. 图片存储格式:JPG/PNG 是 sRGB 编码(gamma ≈ 2.2)
  2. Three.js 自动转换:采样时自动将 sRGB → Linear
  3. Shader 中拿到的:已经是线性空间的颜色值

📐 什么是 sRGB 和 Linear?

颜色空间 说明 特点
sRGB 人眼感知的颜色空间 非线性,暗部压缩,亮部展开
Linear 物理光强的线性空间 线性,适合光照计算

关键点:人眼对亮度的感知是非线性的(史蒂文斯幂定律),而物理光照计算需要在线性空间进行。

🔄 颜色空间转换公式

// sRGB → Linear (解码)
vec3 linear = pow(srgb.rgb, vec3(2.2));

// Linear → sRGB (编码)
vec3 srgb = pow(linear.rgb, vec3(1.0/2.2));

💡 为什么贴图会偏暗?

场景还原

// Three.js 内部做了这件事:
vec4 sampled = texture2D(texture, vUv);  // 返回的是 Linear 空间颜色

// 然后我们直接输出:
gl_FragColor = sampled;  // ❌ 问题:Linear 颜色直接输出到 sRGB 帧缓冲

结果:线性空间的颜色值(如 0.5)直接显示在屏幕上,人眼感知会比预期的暗很多。

🧪 数值对比

原图 sRGB Linear 空间 直接输出到屏幕(错误) 正确输出到屏幕
0.5 (中灰) 0.217 显示为 0.217 (很暗) 应显示为 0.5
0.8 (亮灰) 0.578 显示为 0.578 (偏暗) 应显示为 0.8

看到了吗? 0.5 的中灰色在 Linear 空间只有 0.217,直接输出就变成了"深灰"!


✅ 解决方案

手动 Gamma 编码(推荐)

保留 Three.js 的自动转换,在 Shader 中手动做 Gamma 编码:

uniform sampler2D texture;
varying vec2 vUv;

void main() {
  vec4 color = texture2D(texture, vUv);      // Linear 空间
  color.rgb = pow(color.rgb, vec3(1.0/2.2)); // Linear → sRGB
  gl_FragColor = color;
}

或者封装成函数更清晰:

vec3 linearToSRGB(vec3 linear) {
  return pow(linear, vec3(1.0/2.2));
}

void main() {
  vec4 color = texture2D(texture, vUv);
  color.rgb = linearToSRGB(color.rgb);
  gl_FragColor = color;
}

优点:符合图形学最佳实践,后续加光照也方便
缺点:需要理解颜色空间概念


方案 3:使用内置工具函数(Three.js r152+)

Three.js 提供了内置的颜色空间转换函数:

#include <color_space_pars_fragment>

uniform sampler2D texture;
varying vec2 vUv;

void main() {
  vec4 color = texture2D(texture, vUv);
  color = SRGBToLinear(color);  // 如果需要在线性空间计算
  // ... 光照计算 ...
  color = LinearToSRGB(color);  // 最后转回 sRGB
  gl_FragColor = color;
}

🎯 实战:完整的贴图材质

import * as THREE from 'three';

const vertexShader = `
  varying vec2 vUv;
  
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragmentShader = `
  uniform sampler2D texture;
  varying vec2 vUv;
  
  // Linear → sRGB
  vec3 linearToSRGB(vec3 linear) {
    return pow(linear, vec3(1.0/2.2));
  }
  
  void main() {
    vec4 color = texture2D(texture, vUv);      // Three.js 已自动转为 Linear
    color.rgb = linearToSRGB(color.rgb);       // 手动转回 sRGB
    gl_FragColor = color;
  }
`;

const texture = new THREE.TextureLoader().load('image.jpg');
// texture.colorSpace = THREE.SRGBColorSpace; // 默认就是这个,不用设置

const material = new THREE.ShaderMaterial({
  uniforms: {
    texture: { value: texture }
  },
  vertexShader,
  fragmentShader,
  transparent: true
});

const plane = new THREE.Mesh(
  new THREE.PlaneGeometry(10, 10),
  material
);
scene.add(plane);

🤔 为什么不用乘法,而用幂函数?

这是个好问题!为什么 Gamma 矫正要用 pow(x, 1/2.2) 而不是简单的 x * 0.5

人眼感知的非线性

人眼对亮度的感知符合史蒂文斯幂定律

主观亮度 ∝ (物理光强)^0.4~0.5

这意味着:

  • 物理光强增加 4 倍,人眼只感觉"亮了约 2 倍"
  • 暗部的变化人眼更敏感,亮部的变化相对迟钝

线性乘法的灾难

// ❌ 线性乘法:所有亮度等比例压缩
vec3 dark = color * 0.5;

// ✅ Gamma:非线性压缩,匹配人眼感知
vec3 gamma = pow(color, vec3(1.0/2.2));
操作 暗部 (0.1) 中灰 (0.5) 亮部 (0.9) 人眼感知
×0.5 0.05 0.25 0.45 暗部细节丢失严重 ❌
^0.45 0.28 0.71 0.95 均匀压缩感知亮度 ✅

结论:幂函数是对人眼生物特性的数学拟合,不是随便选的!


📊 方案对比总结

方案 适用场景 优点 缺点
手动 Gamma 编码 通用推荐 符合图形学规范,灵活 需要理解概念
内置工具函数 Three.js r152+ 官方支持,代码简洁 版本限制

💡 实用建议

1. 调试技巧

在 Shader 中临时验证:

// 输出纯中灰(应该看起来是 50% 灰)
gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);  // ❌ 线性 0.5 会很暗

// 正确的 50% 视觉灰
gl_FragColor = vec4(0.73, 0.73, 0.73, 1.0); // ✅ ≈ pow(0.5, 1.0/2.2)

2. 标注工具场景

如果你在做标注工具,只是显示贴图:

// 推荐:禁用自动转换,简单高效
texture.colorSpace = THREE.NoColorSpace;

3. 可视化场景

如果需要光照、阴影等效果:

// 推荐:手动 Gamma 编码
vec4 color = texture2D(texture, vUv);
color.rgb = pow(color.rgb, vec3(1.0/2.2));

🎓 总结

贴图偏暗的问题,本质是颜色空间转换的理解问题:

  1. Three.js 默认:sRGB → Linear(自动)
  2. 自定义 Shader:需要手动将 Linear → sRGB
  3. Gamma 矫正:用幂函数 pow(x, 1/2.2) 而不是乘法,因为人眼感知是非线性的

理解了这个原理,以后写自定义材质就不会再踩坑了!


📚 延伸阅读


💬 互动时间:你在 Three.js 开发中还遇到过哪些"坑"?欢迎在评论区分享!

👍 如果觉得有用,记得点赞收藏,关注我获取更多图形学干货!


本文作者:红波 | 专注 WebGL/Three.js/可视化开发 | 智驾标注工具开发者

WebGL 基础API详解:掌握3D图形编程的核心工具

2026年2月11日 17:59

WebGL API总览

WebGL提供了丰富的API来控制GPU进行3D图形渲染。这些API可以按功能分为几大类,每一类都承担着不同的职责,共同协作完成从数据输入到图像输出的整个渲染流程。

上下文和状态管理API

在开始绘制之前,我们首先需要获取WebGL的绘图上下文,并管理绘图状态。

获取WebGL上下文

// 从canvas元素获取WebGL绘图环境
const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

视口设置

// 设置渲染区域的大小和位置(通常是整个canvas)
gl.viewport(0, 0, canvas.width, canvas.height);

清空缓冲区

// 设置清空颜色(这里是黑色)
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

// 如果启用了深度测试,还需清空深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

功能开关

// 启用深度测试
gl.enable(gl.DEPTH_TEST);

// 启用面剔除
gl.enable(gl.CULL_FACE);

// 启用混合(透明度处理)
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

// 禁用功能
gl.disable(gl.DEPTH_TEST);

着色器相关API

着色器是WebGL的灵魂,这些API负责创建、编译和管理着色器程序。

创建和编译顶点着色器

// 创建顶点着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);

// 设置着色器源代码
const vertexShaderSource = `
attribute vec4 a_position;
void main() {
  gl_Position = a_position;
}
`;
gl.shaderSource(vertexShader, vertexShaderSource);

// 编译着色器
gl.compileShader(vertexShader);

// 检查编译是否成功
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
  console.error('顶点着色器编译失败:', gl.getShaderInfoLog(vertexShader));
}

创建和编译片元着色器

// 创建片元着色器对象
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

// 设置着色器源代码
const fragmentShaderSource = `
precision mediump float;
uniform vec4 u_color;
void main() {
  gl_FragColor = u_color;
}
`;
gl.shaderSource(fragmentShader, fragmentShaderSource);

// 编译着色器
gl.compileShader(fragmentShader);

// 检查编译是否成功
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
  console.error('片元着色器编译失败:', gl.getShaderInfoLog(fragmentShader));
}

程序相关API

程序对象是顶点着色器和片元着色器的组合,这些API负责管理和链接着色器。

创建和链接程序

// 创建程序对象
const program = gl.createProgram();

// 将着色器附加到程序
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// 链接程序
gl.linkProgram(program);

// 检查链接是否成功
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error('程序链接失败:', gl.getProgramInfoLog(program));
}

// 使用程序
gl.useProgram(program);

变量定位和赋值API

这些API用于在JavaScript和着色器之间传递数据,是实现动态渲染的关键。

获取变量位置

// 获取attribute变量位置(顶点着色器中的变量)
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
const colorUniformLocation = gl.getUniformLocation(program, 'u_color');

给attribute变量赋值

// 给attribute变量赋值(1-4个浮点数)
gl.vertexAttrib1f(location, value);  // 1个浮点数
gl.vertexAttrib2f(location, x, y);  // 2个浮点数
gl.vertexAttrib3f(location, x, y, z);  // 3个浮点数
gl.vertexAttrib4f(location, x, y, z, w);  // 4个浮点数

给uniform变量赋值

// 给uniform变量赋值(浮点数)
gl.uniform1f(colorUniformLocation, value);  // 1个浮点数
gl.uniform2f(colorUniformLocation, x, y);  // 2个浮点数
gl.uniform3f(colorUniformLocation, r, g, b);  // 3个浮点数
gl.uniform4f(colorUniformLocation, r, g, b, a);  // 4个浮点数

// 给uniform变量赋值(整数)
gl.uniform1i(textureUniformLocation, textureUnit);  // 1个整数

// 给矩阵uniform变量赋值
const matrix = new Float32Array([
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
]);
gl.uniformMatrix4fv(matrixUniformLocation, false, matrix);

缓冲区相关API

缓冲区用于高效地向GPU传输大量顶点数据,是高性能渲染的基础。

创建和绑定缓冲区

// 创建缓冲区对象
const buffer = gl.createBuffer();

// 绑定缓冲区(指定缓冲区类型)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);  // 顶点数据
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);  // 索引数据

向缓冲区写入数据

// 创建顶点数据
const vertices = new Float32Array([
  -0.5, -0.5,  // 第一个点
   0.5, -0.5,  // 第二个点
   0.0,  0.5   // 第三个点
]);

// 向当前绑定的缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// STATIC_DRAW: 数据一次性写入,多次使用
// DYNAMIC_DRAW: 数据频繁更改
// STREAM_DRAW: 数据偶尔更改

配置顶点属性指针

// 启用顶点属性数组
gl.enableVertexAttribArray(positionAttributeLocation);

// 配置顶点属性
gl.vertexAttribPointer(
  positionAttributeLocation, // attribute变量位置
  2,                         // 每个顶点包含2个分量(x, y)
  gl.FLOAT,                  // 数据类型为浮点数
  false,                     // 不标准化
  0,                         // 步长(0表示紧密排列)
  0                          // 偏移量
);

绘制相关API

这些API触发GPU执行渲染操作。

绘制图元

// 使用顶点数组绘制
gl.drawArrays(
  gl.TRIANGLES,  // 图元类型
  0,             // 起始顶点索引
  3              // 顶点数量
);

// 使用索引数组绘制
const indices = new Uint16Array([0, 1, 2]);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
gl.drawElements(
  gl.TRIANGLES,  // 图元类型
  3,             // 要绘制的索引数量
  gl.UNSIGNED_SHORT, // 索引数据类型
  0               // 偏移量
);

纹理相关API

纹理用于为3D模型添加细节和真实感。

创建和配置纹理

// 创建纹理对象
const texture = gl.createTexture();

// 绑定纹理
gl.bindTexture(gl.TEXTURE_2D, texture);

// 设置纹理图像
gl.texImage2D(
  gl.TEXTURE_2D,    // 纹理目标
  0,                // 纹理级别
  gl.RGBA,          // 内部格式
  gl.RGBA,          // 源格式
  gl.UNSIGNED_BYTE, // 源数据类型
  imageData         // 图像数据
);

// 设置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

// 激活纹理单元
gl.activeTexture(gl.TEXTURE0);  // 激活纹理单元0

混合和深度测试API

这些API控制像素的混合方式和深度测试行为。

混合设置

// 启用混合
gl.enable(gl.BLEND);

// 设置混合函数
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);  // 标准alpha混合

深度测试设置

// 启用深度测试
gl.enable(gl.DEPTH_TEST);

// 设置深度测试函数
gl.depthFunc(gl.LEQUAL);  // 小于等于时通过测试

// 设置深度缓冲写入掩码
gl.depthMask(true);  // 允许写入深度缓冲

常用常量参考

着色器类型

  • gl.VERTEX_SHADER - 顶点着色器
  • gl.FRAGMENT_SHADER - 片元着色器

缓冲区类型

  • gl.ARRAY_BUFFER - 顶点数据缓冲区
  • gl.ELEMENT_ARRAY_BUFFER - 索引数据缓冲区

缓冲区使用方式

  • gl.STATIC_DRAW - 静态数据,一次性写入
  • gl.DYNAMIC_DRAW - 动态数据,频繁更改
  • gl.STREAM_DRAW - 流数据,偶尔更改

绘制模式

  • gl.POINTS - 点
  • gl.LINES - 线段
  • gl.TRIANGLES - 三角形

数据类型

  • gl.FLOAT - 浮点数
  • gl.UNSIGNED_BYTE - 无符号字节

纹理类型

  • gl.TEXTURE_2D - 2D纹理
  • gl.TEXTURE_CUBE_MAP - 立方体贴图

缓冲区位

  • gl.COLOR_BUFFER_BIT - 颜色缓冲区
  • gl.DEPTH_BUFFER_BIT - 深度缓冲区

实际应用示例

回顾我们在前一节中使用的API:

// 使用着色器程序
gl.useProgram(program);

// 获取变量位置
const positionLocation = gl.getAttribLocation(program, 'a_Position');
const colorLocation = gl.getUniformLocation(program, 'u_Color');

// 给attribute变量赋值
gl.vertexAttrib2f(sizeLocation, canvas.width, canvas.height);

// 给uniform变量赋值
gl.uniform4f(colorLocation, r, g, b, a);

// 执行绘制
gl.drawArrays(gl.POINTS, 0, 1);

// 设置清空颜色
gl.clearColor(r, g, b, a);

// 清空缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

掌握了这些基础API,你就能构建各种复杂的3D图形应用了。这些API看似繁多,但它们都有明确的职责分工,一旦熟悉了它们的作用,WebGL编程就会变得清晰明了。

WebGL 从零开始:绘制你的第一个3D点

2026年2月11日 17:36

WebGL程序的两大核心组件

WebGL程序就像一台精密的机器,需要两个关键部件协同工作才能正常运行:

JavaScript程序 - 负责控制和数据处理

着色器程序 - 负责图形渲染

这两个部分缺一不可,就像汽车需要发动机和方向盘一样。

从最简单的红点开始

我们的第一个目标很明确:在屏幕中心绘制一个红色的点,大小为10像素。

ScreenShot_2026-02-11_173444_693.png

顶点着色器:告诉GPU在哪里画点

void main(){
    // 告诉GPU在裁剪坐标系原点画点
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
    // 设置点的大小为10像素
    gl_PointSize = 10.0;
}

这里用到了GLSL语言的几个重要概念:

  • gl_Position是内置变量,用来设置顶点位置
  • gl_PointSize专门控制点的大小
  • vec4是包含4个浮点数的向量容器

片元着色器:告诉GPU用什么颜色画

void main(){
    // 设置像素颜色为红色
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 
}

注意颜色值的表示方法:

  • WebGL中颜色分量范围是0-1,不是0-255
  • 红色表示为(1.0, 0.0, 0.0, 1.0)
  • 对应CSS中的rgb(255, 0, 0)

完整的HTML结构

<body>
    <!-- 顶点着色器源码 -->
    <script type="shader-source" id="vertexShader">
     void main(){
        gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
        gl_PointSize = 10.0;
    }
    </script>
    
    <!-- 片元着色器源码 -->
    <script type="shader-source" id="fragmentShader">
     void main(){
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 
    }
    </script>
    
    <canvas id="canvas"></canvas>
</body>

JavaScript核心代码实现

第一步:获取WebGL绘图环境

// 获取canvas元素
var canvas = document.querySelector('#canvas');
// 获取WebGL上下文(兼容处理)
var gl = canvas.getContext('webgl') || canvas.getContext("experimental-webgl");

第二步:创建和编译着色器

// 获取顶点着色器源码
var vertexShaderSource = document.querySelector('#vertexShader').innerHTML;
// 创建顶点着色器对象
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 将源码分配给着色器对象
gl.shaderSource(vertexShader, vertexShaderSource);
// 编译顶点着色器程序
gl.compileShader(vertexShader);

// 片元着色器创建过程类似
var fragmentShaderSource = document.querySelector('#fragmentShader').innerHTML;
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);

第三步:创建和链接着色器程序

// 创建着色器程序
var program = gl.createProgram();
// 将顶点着色器挂载到程序上
gl.attachShader(program, vertexShader); 
// 将片元着色器挂载到程序上
gl.attachShader(program, fragmentShader);
// 链接着色器程序
gl.linkProgram(program);
// 启用着色器程序
gl.useProgram(program);

第四步:执行绘制操作

// 设置清空画布颜色为黑色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 用设置的颜色清空画布
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制点图元
gl.drawArrays(gl.POINTS, 0, 1);

让点动起来:交互式绘制

静态点不够有趣,让我们实现点击画布就在点击位置绘制彩色点的功能。

改进的着色器程序

// 顶点着色器 - 接收外部数据
precision mediump float;
// 接收点在canvas坐标系上的坐标
attribute vec2 a_Position;
// 接收canvas的宽高尺寸
attribute vec2 a_Screen_Size;

void main(){
    // 将屏幕坐标转换为裁剪坐标
    vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0; 
    position = position * vec2(1.0, -1.0);
    gl_Position = vec4(position, 0, 1);
    gl_PointSize = 10.0;
}

// 片元着色器 - 接收颜色数据
precision mediump float;
// 接收JavaScript传过来的颜色值
uniform vec4 u_Color;

void main(){
    // 将颜色值转换为WebGL格式
    vec4 color = u_Color / vec4(255, 255, 255, 1);
    gl_FragColor = color; 
}

交互式JavaScript实现

// 获取着色器变量位置
var a_Position = gl.getAttribLocation(program, 'a_Position');
var a_Screen_Size = gl.getAttribLocation(program, 'a_Screen_Size');
var u_Color = gl.getUniformLocation(program, 'u_Color');

// 传递canvas尺寸信息
gl.vertexAttrib2f(a_Screen_Size, canvas.width, canvas.height);

// 存储点击位置的数组
var points = [];

// 绑定点击事件
canvas.addEventListener('click', e => {
    var x = e.pageX;
    var y = e.pageY;
    var color = {r: Math.random()*255, g: Math.random()*255, b: Math.random()*255, a: 255};
    points.push({ x: x, y: y, color: color });
    
    // 清空画布并重新绘制所有点
    gl.clearColor(0, 0, 0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    for (let i = 0; i < points.length; i++) {
        // 传递颜色数据
        gl.uniform4f(u_Color, points[i].color.r, points[i].color.g, points[i].color.b, points[i].color.a);
        // 传递位置数据
        gl.vertexAttrib2f(a_Position, points[i].x, points[i].y);
        // 绘制点
        gl.drawArrays(gl.POINTS, 0, 1);
    }
});

核心知识点总结

GLSL语言基础

  • attribute变量:只能在顶点着色器中使用,接收顶点数据
  • uniform变量:可在顶点和片元着色器中使用,接收全局数据
  • varying变量:在顶点着色器和片元着色器间传递插值数据
  • 向量运算:vec2、vec3、vec4等容器类型的运算规则

WebGL API核心方法

  • createShader():创建着色器对象
  • shaderSource():提供着色器源码
  • compileShader():编译着色器
  • createProgram():创建着色器程序
  • attachShader():绑定着色器到程序
  • linkProgram():链接着色器程序
  • useProgram():启用着色器程序
  • drawArrays():执行绘制操作

坐标系转换

从canvas坐标系到裁剪坐标系的转换公式:

position = (canvas_position / canvas_size) * 2.0 - 1.0

这个转换将浏览器坐标映射到WebGL的标准化设备坐标系(NDC),其中x、y坐标范围都是[-1, 1]。

性能优化提示

直接使用gl.vertexAttrib2f逐个传递数据效率较低。后续章节我们会学习使用缓冲区(Buffer)来批量传递顶点数据,这能显著提升渲染性能。

现在你已经掌握了WebGL的基础绘制技能,准备好迎接更复杂的三角形绘制挑战了吗?

WebGL 初探:让你的网页拥有3D魔法

2026年2月11日 17:10

WebGL的前世今生:从插件时代到开放标准

还记得那些年我们用过的Flash吗?在WebGL出现之前,如果想在网页上展示3D效果,我们不得不依赖Adobe Flash、微软SilverLight这些浏览器插件。就像过去我们想要听音乐必须安装专门的播放器一样,这些插件就像一道门槛,限制了3D网页的发展。

但是,聪明的程序员们不甘于此!他们联手打造了一个开放标准——WebGL,让我们的浏览器原生支持3D图形渲染,再也不需要额外安装任何插件了。

WebGL到底是什么?用最简单的话说清楚

想象一下,你正在玩一个3D游戏,那些栩栩如生的场景、流畅的角色动作,背后都是GPU(显卡)在默默工作。WebGL就是一座桥梁,让你可以用JavaScript直接和GPU对话,告诉它"嘿,帮我渲染一个红色的三角形"或者"给我一个旋转的立方体"。

简单来说:

  • WebGL是一套3D图形API(应用程序接口)
  • 它让你的JavaScript代码可以直接控制GPU
  • 你可以用它创建3D图表、网页游戏、3D地图、虚拟现实等精彩应用

WebGL的工作原理:就像工厂的流水线

想象一下汽车制造厂的流水线:

  1. 工人准备好零件(顶点数据)
  2. 每个工位对零件进行加工(顶点着色器)
  3. 零件被组装成车门(图元装配)
  4. 车门表面涂漆(光栅化)
  5. 最后贴上标志(片元着色器)

WebGL的渲染过程也是这样:

// 这是JavaScript部分,准备数据
const vertices = [
  -0.5, -0.5,  // 三角形左下角
   0.5, -0.5,  // 三角形右下角
   0.0,  0.5   // 三角形顶部
];

// 这是顶点着色器,告诉GPU如何放置顶点
const vertexShaderSource = `
attribute vec2 a_position;  // 接收顶点位置
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);  // 设置顶点位置
}
`;

// 这是片元着色器,告诉GPU如何上色
const fragmentShaderSource = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);  // 设置为红色
}
`;

渲染管线的四个关键步骤:

  1. 顶点着色器:处理每个顶点的位置
  2. 图元装配:把顶点连成三角形
  3. 光栅化:把三角形变成像素
  4. 片元着色器:给每个像素上色

wechat_2026-02-11_164451_306.png

WebGL开发需要掌握的技能

如果你已经会HTML、CSS、JavaScript,那么恭喜你,你已经完成了80%的准备工作!WebGL开发还需要:

1. HTML基础(你已经有了)

只需要知道怎么使用<canvas>标签就够了:

<canvas id="webgl-canvas" width="800" height="600"></canvas>

2. JavaScript(你已经精通了)

负责:

  • 获取WebGL上下文
  • 处理顶点数据(坐标、颜色、法向量等)
  • 将数据传递给GPU
  • 加载和解析模型文件

3. GLSL着色器语言(新的挑战)

这是运行在GPU上的小程序,语法类似C语言,但专门为图形处理设计:

// 顶点着色器示例
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
  gl_Position = u_matrix * position;  // 矩阵变换
}

4. 3D数学知识(最重要的基础)

  • 向量:表示方向和距离
  • 矩阵:进行坐标变换(平移、旋转、缩放)
  • 这是WebGL的核心,理解了数学原理,你就掌握了3D世界的钥匙

着色器程序:让GPU听懂你的话

WebGL中有两种着色器:

顶点着色器(Vertex Shader)

  • 处理每个顶点的位置信息
  • 执行坐标变换(移动、旋转、缩放等)

片元着色器(Fragment Shader)

  • 决定每个像素的颜色
  • 处理光照、纹理等效果
// 完整的简单示例
function initWebGL() {
  const canvas = document.getElementById('webgl-canvas');
  const gl = canvas.getContext('webgl');
  
  if (!gl) {
    alert('你的浏览器不支持WebGL');
    return;
  }
  
  // 创建着色器程序...
  // 绘制图形...
}

WebGL的魅力在哪里?

想象一下你能做到的事情:

  • 创建震撼的3D数据可视化
  • 开发浏览器内的3D游戏
  • 构建虚拟现实体验
  • 实现复杂的图像处理效果
  • 优化传统2D应用性能

总结:开启3D世界的大门

WebGL不仅仅是技术,更是创造力的延伸。它让你有能力:

  • 用JavaScript控制GPU的强大计算能力
  • 将数学知识转化为视觉艺术
  • 为用户提供沉浸式的3D体验
  • 在浏览器中实现以前不可能的效果

记住,WebGL的学习曲线虽然陡峭,但一旦掌握,你就能创造出令人惊叹的视觉效果。让我们一起踏上这段精彩的3D之旅吧!

Three.js 透视相机完全指南:从入门到精通

作者 烛阴
2026年2月6日 22:25

什么是透视相机?

生活中的类比

想象你拿着一部手机摄像头对着一个房间:

  • 📱 手机摄像头 = Three.js 中的相机
  • 🎬 摄像头能看到的范围 = 视锥体(Frustum)
  • 📐 摄像头的视角宽度 = 视野角度(FOV)

透视相机就像真实的摄像头一样,离物体越近,看到的范围越小;离物体越远,看到的范围越大。这就是"透视"的含义。

代码示例

import * as THREE from 'three';

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
    40,      // 视野角度(FOV)
    2,       // 宽高比(aspect ratio)(一般3D游戏中的屏幕适配都是在调整这个值)
    0.1,     // 近裁剪面(near)
    1000     // 远裁剪面(far)
);

camera.position.z = 120; // 相机位置

四个关键参数详解

参数 1️⃣:视野角度(FOV - Field of View)

定义:相机能看到的垂直视角范围,单位是度数(°)。

直观理解

  • 🔭 FOV = 10° → 像用望远镜看,视角很窄,看到的东西很小但很清晰
  • 📷 FOV = 40° → 像用普通手机摄像头,视角适中
  • 🐟 FOV = 90° → 像用鱼眼镜头,视角很宽,看到很多东西但会变形
// 小视角 - 看得清楚但范围小
const camera1 = new THREE.PerspectiveCamera(20, 2, 0.1, 1000);

// 中等视角 - 平衡
const camera2 = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);

// 大视角 - 看得多但容易变形
const camera3 = new THREE.PerspectiveCamera(75, 2, 0.1, 1000);

实际应用

  • 🎮 第一人称游戏:FOV 通常 60-90°
  • 🏢 建筑可视化:FOV 通常 40-50°
  • 🎥 电影效果:FOV 通常 24-35°

参数 2️⃣:宽高比(Aspect Ratio)

定义:相机画面的宽度与高度的比例

计算方式

const aspect = window.innerWidth / window.innerHeight;
// 例如:1920 / 1080 = 1.777...

为什么重要

  • ✅ 如果 aspect 与实际画布比例一致,画面不会被拉伸
  • ❌ 如果不一致,圆形会变成椭圆,正方形会变成矩形
// 假设窗口是 1920×1080
const aspect = 1920 / 1080; // ≈ 1.78

const camera = new THREE.PerspectiveCamera(40, aspect, 0.1, 1000);

响应式设计

function onWindowResize() {
    const width = window.innerWidth;
    const height = window.innerHeight;
    
    camera.aspect = width / height;
    camera.updateProjectionMatrix(); // 重要!更新投影矩阵
    
    renderer.setSize(width, height);
}

window.addEventListener('resize', onWindowResize);

参数 3️⃣:近裁剪面(Near)

定义:相机能看到的最近距离。比这个距离更近的物体不会被显示。

为什么需要它

  • 🎯 性能优化:不渲染相机背后的物体
  • 🔍 避免穿模:防止相机进入物体内部时看到内部结构
// near = 0.1 意味着距离相机 0.1 单位以内的物体看不到
const camera = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);

// 如果物体在 z = 0.05,它会被裁剪掉
const cube = new THREE.Mesh(geometry, material);
cube.position.z = 0.05; // ❌ 看不到

参数 4️⃣:远裁剪面(Far)

定义:相机能看到的最远距离。比这个距离更远的物体不会被显示。

为什么需要它

  • 🎯 性能优化:不渲染太远的物体
  • 🔍 深度精度:提高深度缓冲的精度
// far = 1000 意味着距离相机 1000 单位以外的物体看不到
const camera = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);

// 如果物体在 z = 1500,它会被裁剪掉
const star = new THREE.Mesh(geometry, material);
star.position.z = 1500; // ❌ 看不到

视锥体的可视范围计算

什么是视锥体?

视锥体是一个四棱锥形的空间,只有在这个空间内的物体才能被看到。

计算可视范围的公式

在距离相机 distance 处,可视范围的大小为:

垂直高度 = 2 × tan(FOV/2) × distance
水平宽度 = 垂直高度 × aspect

实际计算示例

假设我们有这样的相机配置:

const fov = 40;           // 视野角度
const aspect = 2;         // 宽高比(2:1)
const distance = 120;     // 相机距离(z = 120)

// 计算垂直可视高度
const vFOV = fov * Math.PI / 180; // 转换为弧度
const height = 2 * Math.tan(vFOV / 2) * distance;
// height = 2 × tan(20°) × 120
// height = 2 × 0.364 × 120
// height ≈ 87.2

// 计算水平可视宽度
const width = height * aspect;
// width = 87.2 × 2
// width ≈ 174.4

结论:在 z=120 处,相机能看到的范围是:

  • 📏 宽度:174.4 单位
  • 📏 高度:87.2 单位

坐标范围的确定

场景布局示例

假设我们要在场景中放置一个 5×4 的网格(5列 4行),每个物体之间间距为 15 单位:

const spread = 15; // 间距

// 物体位置计算
for (let x = -2; x <= 2; x++) {
    for (let y = -1; y <= 2; y++) {
        const obj = createObject();
        obj.position.x = x * spread;  // x: -30, -15, 0, 15, 30
        obj.position.y = y * spread;  // y: -15, 0, 15, 30
        scene.add(obj);
    }
}

对象分布范围

  • X 轴:-30 到 30(宽度 60)
  • Y 轴:-15 到 30(高度 45)

检查是否超出可视范围

// 相机可视范围
const visibleWidth = 174.4;
const visibleHeight = 87.2;

// 对象范围
const objectWidth = 60;
const objectHeight = 45;

// 检查
console.log(`宽度是否超出: ${objectWidth > visibleWidth}`); // false ✅
console.log(`高度是否超出: ${objectHeight > visibleHeight}`); // false ✅

如果超出范围怎么办?

问题:当 spread=20 时,对象范围变为 80×60,超出了可视范围。

解决方案

方案 1:增加相机距离

// 原来
camera.position.z = 120;

// 改为
camera.position.z = 160; // 距离越远,看到的范围越大

方案 2:增加视野角度

// 原来
const fov = 40;

// 改为
const fov = 60; // 视角越大,看到的范围越大

方案 3:减小间距

// 原来
const spread = 20;

// 改为
const spread = 15; // 物体靠得更近

方案 4:使用正交相机

如果你不需要透视效果,可以使用正交相机(OrthographicCamera),它的可视范围不会随距离变化:

const camera = new THREE.OrthographicCamera(
    -100,  // left
    100,   // right
    50,    // top
    -50,   // bottom
    0.1,   // near
    1000   // far
);

实战代码

完整的响应式相机设置

import * as THREE from 'three';

class ResponsiveCamera {
    constructor() {
        this.fov = 40;
        this.near = 0.1;
        this.far = 1000;
        this.distance = 120;
        
        this.updateCamera();
    }
    
    updateCamera() {
        const aspect = window.innerWidth / window.innerHeight;
        
        this.camera = new THREE.PerspectiveCamera(
            this.fov,
            aspect,
            this.near,
            this.far
        );
        
        this.camera.position.z = this.distance;
    }
    
    // 计算指定深度处的可视范围
    getVisibleRange(depth = null) {
        const vFOV = (this.fov * Math.PI) / 180;
        // 如果没有指定深度,使用相机的默认距离
        const distance = depth !== null ? depth : this.distance;
        const height = 2 * Math.tan(vFOV / 2) * distance;
        const width = height * (window.innerWidth / window.innerHeight);

        return { width, height };
    }
    
    // 检查物体是否在可视范围内
    isObjectVisible(obj) {
        const pos = obj.position;
        
        // 计算物体相对于相机的距离(沿着相机的视线方向)
        const distanceFromCamera = this.camera.position.z - pos.z;
        
        // 计算物体所在深度的可视范围
        const range = this.getVisibleRange(distanceFromCamera);
        console.log('物体距离相机:', distanceFromCamera, '该深度的可视范围:', range);

        return (
            Math.abs(pos.x) <= range.width / 2 &&
            Math.abs(pos.y) <= range.height / 2 &&
            distanceFromCamera >= this.near &&
            distanceFromCamera <= this.far
        );
    }
    
    // 窗口大小改变时更新
    onWindowResize() {
        this.updateCamera();
        this.camera.updateProjectionMatrix();
    }
}

// 使用示例
// 使用示例
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);

// 4. 创建渲染器:将3D场景渲染到网页上
const renderer = new THREE.WebGLRenderer({ antialias: true }); // antialias开启抗锯齿
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染尺寸
// 将渲染器的画布添加到网页中
document.body.appendChild(renderer.domElement);

const camera = new ResponsiveCamera();
window.addEventListener('resize', () => camera.onWindowResize());

// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
scene.add(directionalLight);

// 检查物体是否可见
const cube = new THREE.Mesh(new THREE.BoxGeometry(8, 8, 8), createMaterial());
cube.position.set(20, 20, -10);
scene.add(cube);

function animate(time) {
    renderer.render(scene, camera.getCamera());
    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

console.log(camera.isObjectVisible(cube)); // true or false

小结

  1. FOV 和 distance 决定可视范围

    • FOV 越大,看到的范围越大
    • distance 越大,看到的范围越大
  2. aspect 必须与画布比例一致

    • 否则画面会被拉伸变形
  3. near 和 far 定义了深度范围

    • 在这个范围外的物体看不到

📂 核心代码与完整示例:    my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

❌
❌