Threejs源码系列- MathUtils(1)

mapLinear函数

将一个数值从一个范围线性映射到另一个范围

js 复制代码
/**
 * @param {number} x - 需要被映射的原始数值。
 * @param {number} a1 - 原始范围的最小值。
 * @param {number} a2 - 原始范围的最大值。
 * @param {number} b1 - 目标范围的最小值。
 * @param {number} b2 - 目标范围的最大值。
 * @return {number} 
 */
function mapLinear( x, a1, a2, b1, b2 ) {

	return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );

}

应用场景

在 3D 图形中常用于:

  • 将坐标从一个坐标系转换到另一个坐标系(如屏幕坐标与 3D 世界坐标的映射)。
  • 动画参数映射(如将时间范围映射到物体位置范围)。
  • 颜色值、光照强度等参数的范围转换。

功能说明

mapLinear 实现了线性插值的逆过程,它将输入值 x 从原始范围 [a1, a2] 映射到目标范围 [b1, b2],映射结果保持线性关系。

例如:若 x 是 a1 和 a2 的中点,则映射结果也会是 b1 和 b2 的中点。

实现逻辑

函数通过公式 b1 + (x - a1) * (b2 - b1) / (a2 - a1) 实现映射,步骤如下:

  • 计算 x 在原始范围中的相对位置:(x - a1) / (a2 - a1)。这个值表示 x 距离 a1 的比例(范围通常为 [0, 1],但也支持超出范围的 x)。
  • 将这个比例应用到目标范围的长度 (b2 - b1) 上,得到目标范围中的偏移量。
  • 加上目标范围的最小值 b1,得到最终映射结果。

示例

  • 若 x=2,原始范围 [1, 3],目标范围 [10, 20]:
  • 计算过程:10 + (2-1) * (20-10)/(3-1) = 10 + 1*10/2 = 15,即 2 在 [1,3] 中是中点,映射到 [10,20] 的中点 15。
  • 若 x=0(低于原始范围最小值),上述参数下结果为 5,保持线性趋势。

inverseLerp函数

用于计算一个值在两个已知点之间的相对位置比例,是线性插值(lerp)的逆运算。

js 复制代码
/**
 *
 * @param {number} x - 起始点
 * @param {number} y - 结束点
 * @param {number} value - 起始-结束之间某个点
 * @return {number} 插值系数
 */
function inverseLerp( x, y, value ) {

	if ( x !== y ) {

		return ( value - x ) / ( y - x );

	} else {

		return 0;

	}

}

应用场景

在 3D 图形和动画中常用于:

  • 计算某个值在已知范围内的进度(如动画时间进度、物体位置在两点间的进度)。
  • 将任意范围的数值归一化到 [0, 1] 区间(方便映射到其他范围)。
  • 反向推导线性插值的参数(例如已知插值结果,求插值因子 t)。

功能说明

  • 当已知起始点 x、终点 y,以及一个位于 x 和 y 之间(或之外)的数值 value 时,该函数返回 value 相对于 x 和 y 范围的比例(通常在 [0, 1] 区间内,超出范围时会小于 0 或大于 1)。
  • 例如:若 x=10、y=20,当 value=15 时,返回 0.5(表示在中间位置);当 value=5 时,返回 -0.5(表示在 x 左侧 50% 位置)。

实现逻辑

  • 若 x !== y,通过公式 (value - x) / (y - x) 计算比例。分子是 value 与起始点 x 的差值,分母是 y 与 x 的总距离,结果即为相对比例。
  • 若 x === y(避免除以 0 的错误),直接返回 0。

lerp函数

用于实现线性插值(Linear Interpolation),即根据插值因子 t 在两个已知点 x 和 y 之间计算出一个中间值。

js 复制代码
/**
 *
 * @param {number} x - 插值的起点
 * @param {number} y - 插值的终点
 * @param {number} t - 插值因子,用于控制插值位置,通常在 [0, 1] 区间内
 * @return {number} 
 */
function lerp( x, y, t ) {

	return ( 1 - t ) * x + t * y;

}

应用场景

在 3D 图形和动画中广泛用于:

  • 动画过渡(如物体位置、旋转角度的平滑变化);
  • 颜色渐变(在两个颜色值之间按比例混合);
  • 相机视角切换、灯光强度调整等需要线性过渡的场景。

功能说明

线性插值的核心是根据参数 t 的取值,在起点 x 和终点 y 之间生成一个线性过渡的值:

  • 当 t = 0 时,返回起点 x;
  • 当 t = 1 时,返回终点 y;
  • 当 t 为 0 到 1 之间的数值时,返回 x 和 y 之间的中间值(例如 t = 0.5 时返回 x 和 y 的平均值);
  • 当 t 超出 [0, 1] 范围时,会得到 x 左侧(t < 0)或 y 右侧(t > 1)的外插值,保持线性趋势。

实现逻辑

函数通过公式 (1 - t) * x + t * y 计算结果,原理如下:

  • (1 - t) * x 表示保留起点 x 的比例(随 t 增大而减小);
  • t * y 表示引入终点 y 的比例(随 t 增大而增大);
  • 两者相加即得到 x 到 y 之间按 t 比例过渡的线性插值结果。

示例

  • lerp(10, 20, 0.3):计算过程为 (1 - 0.3) * 10 + 0.3 * 20 = 7 + 6 = 13,即从 10 向 20 插值 30% 得到 13。
  • lerp(5, 15, 1.2):计算过程为 (1 - 1.2) * 5 + 1.2 * 15 = (-0.2)*5 + 18 = -1 + 18 = 17,即超出终点 15 继续插值 20% 得到 17。

damp函数

用于实现基于时间的平滑插值,能让数值从当前值 x 以类弹簧的阻尼效果过渡到目标值 y,且过渡效果不受帧率影响(帧速率无关)。

js 复制代码
/**
 *
 * @param {number} x - 当前值(起始点)
 * @param {number} y - 目标值(终点)。
 * @param {number} lambda - 阻尼系数,控制过渡速度。值越大,过渡越突然(快速接近目标);值越小,过渡越平缓(缓慢接近目标)。
 * @param {number} dt - 时间增量(单位:秒),即当前帧与上一帧的时间间隔,用于保证帧率无关性。
 * @return {number} 
 */
function damp( x, y, lambda, dt ) {

	return lerp( x, y, 1 - Math.exp( - lambda * dt ) );

}

应用场景

在 3D 动画和交互中常用于:

  • 相机跟随目标时的平滑移动。
  • 物体位置、旋转角度的阻尼过渡(如 UI 元素的弹性动画)。
  • 物理模拟中的阻尼效果(如弹簧振动衰减)。

功能说明

函数通过结合线性插值(lerp)和指数衰减,实现平滑的数值过渡:

  • 随着时间推移,当前值 x 会逐渐接近目标值 y,且接近速度会逐渐减慢(阻尼效果)。
  • 即使帧率不稳定(如帧间隔时间不同),也能保证过渡的视觉效果一致。

示例

假设 x=0,y=100,lambda=5,dt=0.1(100 毫秒):

  • 计算衰减因子:Math.exp(-5 * 0.1) = Math.exp(-0.5) ≈ 0.6065
  • 插值参数 t = 1 - 0.6065 ≈ 0.3935
  • 结果:lerp(0, 100, 0.3935) ≈ 39.35(第一帧后接近目标约 39%)

随着时间累积,每次调用都会让当前值更接近 100,且速度逐渐减慢。

pingpong函数

用于实现往返震荡效果,让输入值 x 在 0 和指定的 length 之间来回交替变化,类似乒乓球(ping-pong)的往返运动。

js 复制代码
/**
 *
 * @param {number} x - 输入的基础值(通常是随时间递增的变量,如动画帧编号、时间戳等)。
 * @param {number} [length=1] - 可选参数,默认值为 1,定义了震荡的最大值(即往返区间的上限)。
 * @return {number} 
 */
function pingpong( x, length = 1 ) {

	return length - Math.abs( euclideanModulo( x, length * 2 ) - length );

}

应用场景

在 3D 动画和交互中常用于:

  • 物体的往复运动(如上下摆动的灯光、左右移动的平台)。
  • 颜色或透明度的周期性渐变(如呼吸灯效果)。
  • 粒子系统的震荡行为(如粒子在固定范围内来回移动)。

该函数通过结合欧几里得模运算和绝对值反转,高效实现了无漂移的周期性往返效果。

功能说明

  • 函数的输出值会随 x 的增大呈现周期性的 "往返" 变化:从 0 上升到 length,再下降到 0,重复循环。
  • 例如:当 length=5 时,随着 x 从 0 增大,输出值会从 0 增至 5,再减回 0,然后再次增至 5,以此类推。

实现逻辑

核心公式:length - Math.abs( euclideanModulo( x, length * 2 ) - length ),步骤拆解如下:

  • 计算周期范围:length * 2 定义了一个完整周期的长度(从 0 到 length 再回到 0 为一个周期)。
  • 欧几里得模运算:euclideanModulo( x, length * 2 ) 将 x 映射到 [0, length*2) 区间内(确保周期性)。
  • 中心偏移:euclideanModulo(...) - length 将区间从 [0, length*2) 偏移为 [-length, length),使得中点在 0。
  • 取绝对值:Math.abs(...) 将偏移后的区间转换为 [0, length],形成 "上升" 阶段(从 0 到 length)。
  • 反向输出:length - ... 将结果反转,形成 "下降" 阶段(从 length 到 0),最终实现往返效果。

示例

以 length=5 为例:

  • 当 x=0 时:euclideanModulo(0, 10)=0 → 0-5=-5 → abs(-5)=5 → 5-5=0
  • 当 x=2.5 时:euclideanModulo(2.5, 10)=2.5 → 2.5-5=-2.5 → abs(-2.5)=2.5 → 5-2.5=2.5(上升阶段)
  • 当 x=5 时:euclideanModulo(5, 10)=5 → 5-5=0 → abs(0)=0 → 5-0=5(达到最大值)
  • 当 x=7.5 时:euclideanModulo(7.5, 10)=7.5 → 7.5-5=2.5 → abs(2.5)=2.5 → 5-2.5=2.5(下降阶段)
  • 当 x=10 时:euclideanModulo(10, 10)=0 → 结果回到 0(完成一个周期)

smoothstep函数

用于实现平滑过渡效果,在 [min, max] 区间内对输入值 x 进行平滑的非线性映射,输出值范围为 [0, 1],且在区间两端(接近 min 和 max 时)过渡会减缓,避免突变。

js 复制代码
/**
 * @param {number} x - 区间值
 * @param {number} min - 最小值
 * @param {number} max - 最大值
 * @return {number} 
 */
function smoothstep( x, min, max ) {

	if ( x <= min ) return 0;
	if ( x >= max ) return 1;

	x = ( x - min ) / ( max - min );

	return x * x * ( 3 - 2 * x );

}

应用场景

在 3D 图形和动画中常用于:

  • 平滑的动画过渡(如物体移动、缩放的启停阶段减速);
  • 渐变效果(如颜色、透明度的平滑变化);
  • 碰撞检测中的缓冲区域判断;
  • 相机镜头的平滑切换等需要自然过渡的场景。

该函数的核心优势是相比线性插值(lerp),能产生更自然、无突兀感的过渡效果。

功能说明

  • 当 x 小于等于 min 时,返回 0;
  • 当 x 大于等于 max 时,返回 1;
  • 当 x 在 (min, max) 区间内时,返回一个平滑过渡的值,该值随 x 增大从 0 逐渐增至 1,但增速在两端较慢、中间较快(类似 "S" 形曲线)。

实现逻辑

边界处理:

先判断 x 是否超出 [min, max] 范围,若超出则直接返回边界值(0 或 1)。
2.

归一化:

将 x 映射到 [0, 1] 区间,公式为 (x - min) / (max - min)。例如,若 min=2、max=6、x=4,则归一化后为 (4-2)/(6-2)=0.5。
3.

平滑计算:

使用公式 x * x * (3 - 2 * x) 对归一化后的 x 进行处理,生成平滑曲线:

  • 当 x=0 时,结果为 0;
  • 当 x=1 时,结果为 1;
  • 中间值通过二次函数与线性项的组合,实现 "缓进缓出" 的效果(一阶导数在两端为 0,保证过渡平滑无突变)。

示例

设 min=0,max=10:

  • x=0 → 返回 0;
  • x=5(中间值)→ 归一化后 x=0.5 → 计算 0.50.5 (3-20.5) = 0.252 = 0.5(刚好中间值);
  • x=10 → 返回 1;
  • x=2 → 归一化后 x=0.2 → 计算 0.2²*(3-20.2) = 0.042.6 = 0.104(增速较慢);
  • x=8 → 归一化后 x=0.8 → 计算 0.8²*(3-20.8) = 0.641.4 = 0.896(接近终点时增速放缓)。

smootherstep函数

是 smoothstep 的增强版平滑过渡函数,在 [min, max] 区间内对输入值 x 进行更平滑的非线性映射,输出范围为 [0, 1]。其核心特点是在区间两端(x=0 和 x=1 归一化后)的一阶和二阶导数均为 0,实现比 smoothstep 更柔和的过渡效果。

js 复制代码
/**
 * @param {number} x - 输入值,根据其在 [min, max] 区间的位置计算输出值。
 * @param {number} min - 最小值
 * @param {number} max - 最大值
 * @return {number} 
 */
function smootherstep( x, min, max ) {

	if ( x <= min ) return 0;
	if ( x >= max ) return 1;

	x = ( x - min ) / ( max - min );

	return x * x * x * ( x * ( x * 6 - 15 ) + 10 );

}

应用场景

在 3D 动画、图形渲染中用于:

  • 更细腻的动画过渡(如镜头移动、物体淡入淡出);
  • 流体效果、粒子系统的平滑渐变;
  • 游戏中角色移动的 "缓动 - 加速 - 缓停" 自然运动模拟。

该函数通过高阶多项式实现了无突变、无加速突变的超平滑过渡,是对基础平滑函数的进阶优化。

功能说明

  • 当 x ≤ min 时,返回 0;
  • 当 x ≥ max 时,返回 1;
  • 当 x 在 (min, max) 区间内时,返回一个平滑过渡的值,随 x 增大从 0 逐渐增至 1,且过渡曲线在两端的 "坡度变化率" 为 0(无突变),中间段过渡更自然。

实现逻辑

边界处理:

若 x 超出 [min, max] 范围,直接返回边界值(0 或 1)。
2.

归一化:

将 x 映射到 [0, 1] 区间,公式为 (x - min) / (max - min)。例如,min=2、max=6、x=4 时,归一化后为 0.5。
3.

平滑计算:

使用五阶多项式公式 x³ * (x * (x * 6 - 15) + 10) 处理归一化后的 x,展开后为 6x⁵ - 15x⁴ + 10x³。该公式的特性:

  • 当 x=0 时,结果为 0;当 x=1 时,结果为 1(保证区间两端值正确)。
  • 一阶导数(斜率)在 x=0 和 x=1 时均为 0(无突变)。
  • 二阶导数(斜率的变化率)在 x=0 和 x=1 时也为 0(过渡更平滑,无 "加速突变")。

示例

设 min=0,max=10:

  • x=0 → 返回 0;
  • x=5(中间值)→ 归一化后 x=0.5 → 计算 0.5³ * (0.5*(0.56 - 15) + 10) = 0.125 * (0.5(-12) + 10) = 0.125 * 4 = 0.5(中间值);
  • x=10 → 返回 1;
  • x=2 → 归一化后 x=0.2 → 结果约为 0.02048(增速极缓);
  • x=8 → 归一化后 x=0.8 → 结果约为 0.9728(接近终点时增速极缓)。

与 smoothstep 的对比

  • smoothstep 使用三阶多项式 3x² - 2x³,仅保证一阶导数在两端为 0;
  • smootherstep 使用五阶多项式,额外保证二阶导数为 0,过渡更柔和,适合对平滑度要求更高的场景。

randInt函数

生成一个在 <low, high> 区间内(包含边界值)的随机整数。

js 复制代码
/**
 * @param {number} low - 下限
 * @param {number} high - 上限
 * @return {number} 
 */
function randInt( low, high ) {

	return low + Math.floor( Math.random() * ( high - low + 1 ) );

}

应用场景

在 Three.js 中常用于:

  • 随机生成物体位置、颜色索引等整数参数。
  • 模拟随机事件(如粒子的随机方向、随机数量的元素生成)。
  • 测试场景中生成随机测试数据。

该函数是生成指定范围整数随机数的基础工具,逻辑简洁且能保证均匀分布在目标区间内。

关键细节

  • 函数确保了 low 和 high 两个边界值都有机会被选中(通过 +1 包含完整区间)。
  • 依赖 Math.random() 生成基础随机数,因此结果是伪随机的。

randFloat函数

生成一个在 <low, high> 区间内(包含下限 low,不包含上限 high)的随机浮点数。

js 复制代码
/**
 * @param {number} low - 下限
 * @param {number} high - 上限
 * @return {number}
 */
function randFloat( low, high ) {

	return low + Math.random() * ( high - low );

}

应用场景

在 Three.js 中常用于:

  • 随机生成物体的位置、大小、旋转角度等非整数参数。
  • 模拟自然现象(如粒子的随机速度、颜色的随机渐变值)。
  • 为场景元素添加随机变化(如随机缩放比例、随机透明度)。

该函数是生成指定范围浮点数随机数的基础工具,逻辑简洁且能保证随机性在区间内的均匀分布。

关键细节

  • 函数生成的随机数包含下限 low(当 Math.random() 返回 0 时),但不包含上限 high(因为 Math.random() 最大接近 1 但不等于 1)。
  • 依赖 Math.random() 生成基础随机数,因此结果是伪随机的,均匀分布在目标区间内。

randFloatSpread函数

生成一个在 <-range/2, range/2> 区间内的随机浮点数(包含区间端点附近的值)。

js 复制代码
/**
 * @param {number} range - 随机数的范围跨度
 * @return {number} 
 */
function randFloatSpread( range ) {

	return range * ( 0.5 - Math.random() );

}

应用场景

在 Three.js 中常用于:

  • 生成围绕原点对称分布的随机位置(如粒子系统中粒子在原点周围的随机偏移)。
  • 模拟对称范围内的随机力或速度(如物体在某一轴上的随机抖动)。
  • 为场景元素添加对称的随机变化(如灯光强度的正负波动)。

该函数是生成对称区间随机浮点数的便捷工具,适合需要以原点为中心的随机分布场景。

功能说明

  • 输入参数 range 定义了随机数的范围跨度,函数返回一个大于等于 -range/2 且小于等于 range/2 的随机浮点数。
  • 例如:randFloatSpread(10) 可能返回 -4.9、0、3.2 等,取值范围在 [-5, 5] 之间。

关键细节

  • 生成的随机数关于 0 对称,即正负值出现的概率均等。
  • 结果是浮点数,而非整数(注释中 "returns a random float" 明确这一点)。
  • 相比 randFloat(-range/2, range/2),该函数通过更简洁的公式实现了相同的对称区间随机效果。

seededRandom函数

生成确定性伪随机浮点数(即给定相同可预测的随机数,相同种子会生成相同序列),结果范围为 [0, 1]。

js 复制代码
/**
 * @param {number} [s] - 整数种子
 * @return {number} 
 */
function seededRandom( s ) {

	if ( s !== undefined ) _seed = s;

	// Mulberry32 generator

	let t = _seed += 0x6D2B79F5;

	t = Math.imul( t ^ t >>> 15, t | 1 );

	t ^= t + Math.imul( t ^ t >>> 7, t | 61 );

	return ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296;

}

应用场景

在 Three.js 中常用于:

  • 生成可复现的随机效果(如相同种子生成相同的粒子分布、地形图案)。
  • 测试场景中固定随机输入,确保结果可验证。
  • 多人同步场景中,通过共享种子让不同客户端生成一致的随机内容。

关键特性

  • 确定性:相同种子 s 会生成完全相同的随机序列(例如,两次调用 seededRandom(123) 后,后续随机数完全一致)。
  • 伪随机性:序列看似随机,但可复现,适合需要固定随机效果的场景(如动画回放、 procedural 生成的一致性)。
  • 轻量高效:算法简单,计算快速,适合实时图形场景。

实现逻辑(基于 Mulberry32 算法)

Mulberry32 是一种轻量级伪随机数生成算法,以速度快、随机性较好为特点,适合需要确定性随机的场景。步骤拆解:

  1. 种子初始化 / 更新:

    js 复制代码
    if (s !== undefined) _seed = s; // 若传入种子,则更新内部种子
    let t = _seed += 0x6D2B79F5;   // 种子累加一个固定大整数(0x6D2B79F5),初始化中间变量 t
  2. 第一次变换:

    js 复制代码
    t = Math.imul(t ^ t >>> 15, t | 1);
    • t >>> 15:将 t 无符号右移 15 位(高位补 0)。
    • t ^ (t >>> 15):按位异或操作,打乱比特分布。
    • t | 1:确保结果为奇数(最低位设为 1)。
    • Math.imul(a, b):32 位整数乘法(避免 JavaScript 浮点数精度问题)。
  3. 第二次变换:

    js 复制代码
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    • 类似第一次变换,通过右移 7 位、异或、加法和乘法进一步打乱比特。
    • t | 61 是固定掩码,增强随机性。
  4. 生成最终结果:

    js 复制代码
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
    • t ^ t >>> 14:再次异或操作,调整比特。
    • >>>0:将结果转为无符号 32 位整数(范围 0 到 4294967295)。
    • 除以 4294967296(即 2^32),将范围映射到 [0, 1)(实际因整数上限为 4294967295,结果为 [0, 1])。
相关推荐
逾明15 分钟前
Electron自定义菜单栏及Mac最大化无效的问题解决
前端·electron
辰九九20 分钟前
Uncaught URIError: URI malformed 报错如何解决?
前端·javascript·浏览器
月亮慢慢圆20 分钟前
Echarts的基本使用(待更新)
前端
芜青33 分钟前
实现文字在块元素中水平/垂直居中详解
前端·css·css3
useCallback37 分钟前
Elpis全栈项目总结
前端
小高00743 分钟前
React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
前端·javascript·react.js
LuckySusu1 小时前
【js篇】深入理解类数组对象及其转换为数组的多种方法
前端·javascript
LuckySusu1 小时前
【js篇】数组遍历的方法大全:前端开发中的高效迭代
前端·javascript
LuckySusu1 小时前
【js篇】for...in与 for...of 的区别:前端开发中的迭代器选择
前端·javascript
mon_star°1 小时前
有趣的 npm 库 · json-server
前端·npm·json