WebGL 咖啡蒸汽:3D 粒子与温度模拟
视觉与交互的“像素化美学” WebGL 咖啡蒸汽:3D 粒子与温度模拟
像素化美学在数字艺术中一直占据独特地位,它通过低分辨率、块状元素唤起复古情怀,同时与现代技术结合创造出新的视觉语言。WebGL 技术让这种美学在浏览器中得以实现,尤其是通过 3D 粒子和物理模拟,可以构建出如咖啡蒸汽般细腻的动态效果。温度变化对粒子行为的影响进一步增加了真实感,使交互更加生动。
像素化美学的核心概念
像素化美学源于早期计算机图形学的限制,但如今已成为一种刻意追求的风格。它的特点包括:
- 块状结构:图形由明显可见的像素或体素构成
- 有限调色板:通常使用减少的颜色数量增强复古感
- 锯齿边缘:故意保留阶梯状边缘而非抗锯齿处理
// 简单的像素化着色器示例
const fragmentShader = `
precision mediump float;
uniform sampler2D uTexture;
uniform float uPixelSize;
varying vec2 vUv;
void main() {
vec2 uv = floor(vUv / uPixelSize) * uPixelSize;
vec4 color = texture2D(uTexture, uv);
gl_FragColor = color;
}
`;
WebGL 中的粒子系统实现
WebGL 粒子系统通过顶点着色器高效渲染大量粒子,每个粒子可以代表蒸汽中的一个微小单元。关键实现步骤包括:
- 粒子缓冲区创建:使用
gl.createBuffer()
初始化粒子数据 - 顶点着色器处理:在 GPU 上并行计算粒子位置
- 属性指针设置:通过
gl.vertexAttribPointer()
传递数据
class ParticleSystem {
constructor(gl, count = 1000) {
this.gl = gl;
this.count = count;
this.particles = new Float32Array(count * 3);
// 初始化随机粒子位置
for (let i = 0; i < count; i++) {
this.particles[i * 3] = Math.random() * 2 - 1;
this.particles[i * 3 + 1] = Math.random() * 2 - 1;
this.particles[i * 3 + 2] = Math.random() * 2 - 1;
}
this.buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, this.particles, gl.DYNAMIC_DRAW);
}
update() {
// 粒子更新逻辑
for (let i = 0; i < this.count; i++) {
// 模拟简单的上升运动
this.particles[i * 3 + 1] += 0.01;
if (this.particles[i * 3 + 1] > 1) {
this.particles[i * 3 + 1] = -1;
}
}
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.particles);
}
}
温度场对粒子行为的影响
温度模拟为蒸汽效果增加了物理真实感。可以通过以下方式实现:
- 温度梯度场:定义空间中的温度分布
- 粒子响应函数:根据局部温度调整粒子速度
- 热传导模拟:随时间扩散温度变化
// 顶点着色器中的温度影响
uniform sampler2D uTemperatureField;
uniform float uTime;
attribute vec3 aPosition;
attribute float aParticleSize;
varying vec3 vColor;
void main() {
vec2 texCoord = (aPosition.xy + 1.0) * 0.5;
float temperature = texture2D(uTemperatureField, texCoord).r;
// 温度影响粒子上升速度和颜色
float riseSpeed = 0.1 * temperature;
vec3 position = aPosition + vec3(0.0, riseSpeed * uTime, 0.0);
// 温度映射到颜色 (冷色到暖色)
vColor = mix(vec3(0.3, 0.5, 1.0), vec3(1.0, 0.5, 0.2), temperature);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
gl_PointSize = aParticleSize * (1.0 + temperature * 2.0);
}
交互设计与用户体验优化
良好的交互设计能极大增强视觉效果的表现力:
- 鼠标交互热区:检测用户鼠标位置影响温度场
- 触摸响应:移动设备上的多点触控支持
- 性能平衡:根据设备能力动态调整粒子数量
// 交互式温度场更新
canvas.addEventListener('mousemove', (event) => {
const rect = canvas.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = ((event.clientY - rect.top) / rect.height) * 2 - 1;
// 更新WebGL纹理中的温度数据
updateTemperatureField(x, y, 0.5); // 在鼠标位置增加热度
});
function updateTemperatureField(centerX, centerY, intensity) {
const tempData = new Float32Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = (x / width) * 2 - 1 - centerX;
const dy = (y / height) * 2 - 1 - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 高斯分布的热源
tempData[y * width + x] = intensity * Math.exp(-distance * distance * 10.0);
}
}
gl.bindTexture(gl.TEXTURE_2D, temperatureTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.FLOAT, tempData);
}
性能优化技巧
大规模粒子渲染需要特别注意性能:
- 实例化渲染:使用
ANGLE_instanced_arrays
扩展 - 粒子池:复用不可见粒子而非创建新粒子
- LOD系统:根据视图距离调整粒子细节
// 使用实例化渲染优化性能
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (ext) {
// 设置实例化属性
ext.vertexAttribDivisorANGLE(positionAttrLoc, 1);
ext.vertexAttribDivisorANGLE(colorAttrLoc, 1);
// 绘制调用
ext.drawArraysInstancedANGLE(gl.POINTS, 0, 1, particleCount);
} else {
// 回退到标准渲染
gl.drawArrays(gl.POINTS, 0, particleCount);
}
视觉风格定制
通过参数调整可以创造不同风格的蒸汽效果:
- 复古游戏风格:减少粒子数量,增加像素感
- 写实风格:增加粒子密度,添加光照效果
- 艺术化表现:非自然色彩映射
// 风格化片段着色器
varying vec3 vColor;
uniform float uPixelation;
void main() {
// 应用像素化
vec2 uv = gl_FragCoord.xy / uPixelation;
uv = floor(uv) * uPixelation;
// 限制颜色数量模拟复古风格
vec3 color = floor(vColor * 4.0) / 4.0;
// 添加扫描线效果
float scanline = mod(gl_FragCoord.y, 2.0) * 0.1 + 0.9;
color *= scanline;
gl_FragColor = vec4(color, 1.0);
}
跨浏览器兼容性处理
确保效果在不同平台一致表现:
- WebGL 特性检测:检查扩展和限制
- 回退方案:当WebGL不可用时显示静态图像
- 移动端适配:调整触摸交互和渲染分辨率
// WebGL初始化时的兼容性检查
function initWebGL() {
try {
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
showFallbackImage();
return null;
}
// 检查所需扩展
const requiredExtensions = ['OES_texture_float', 'ANGLE_instanced_arrays'];
for (const extName of requiredExtensions) {
if (!gl.getExtension(extName)) {
console.warn(`Extension ${extName} not available`);
showFallbackImage();
return null;
}
}
return gl;
} catch (e) {
showFallbackImage();
return null;
}
}
function showFallbackImage() {
canvas.style.display = 'none';
const fallback = document.createElement('img');
fallback.src = 'fallback.png';
canvas.parentNode.appendChild(fallback);
}
动态参数调节系统
开发时添加实时调节工具能加速迭代:
- dat.GUI 集成:创建可视化控制面板
- URL参数覆盖:允许通过查询字符串调整参数
- 预设系统:保存和加载不同配置
// 使用dat.GUI创建控制面板
const params = {
particleCount: 5000,
temperatureEffect: 1.0,
pixelSize: 2.0,
riseSpeed: 0.1,
reset: () => initScene()
};
const gui = new dat.GUI();
gui.add(params, 'particleCount', 1000, 20000).step(1000).onChange(updateParticleCount);
gui.add(params, 'temperatureEffect', 0.0, 2.0).onChange(updateShaderUniforms);
gui.add(params, 'pixelSize', 1.0, 8.0).onChange(updateShaderUniforms);
gui.add(params, 'riseSpeed', 0.01, 0.5).onChange(updateShaderUniforms);
gui.add(params, 'reset');
function updateShaderUniforms() {
gl.useProgram(shaderProgram);
gl.uniform1f(uTemperatureEffectLoc, params.temperatureEffect);
gl.uniform1f(uPixelSizeLoc, params.pixelSize);
gl.uniform1f(uRiseSpeedLoc, params.riseSpeed);
}
响应式设计与自适应渲染
确保效果在不同设备上都能良好展现:
- 视口适应:处理窗口大小变化
- 分辨率缩放:根据设备像素比例调整
- 性能自适应:基于帧率动态降级
// 响应式调整
window.addEventListener('resize', debounce(() => {
const width = canvas.clientWidth * window.devicePixelRatio;
const height = canvas.clientHeight * window.devicePixelRatio;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
gl.viewport(0, 0, width, height);
// 调整投影矩阵
const aspect = width / height;
mat4.perspective(projectionMatrix, 45 * Math.PI / 180, aspect, 0.1, 100.0);
// 可能需要重新创建纹理和帧缓冲区
initFramebuffers();
}
}, 200));
// 性能自适应
let lastFrameTime = 0;
function checkPerformance() {
const now = performance.now();
const deltaTime = now - lastFrameTime;
lastFrameTime = now;
// 如果帧率低于30fps,减少粒子数量
if (deltaTime > 33 && params.particleCount > 2000) {
params.particleCount = Math.max(2000, params.particleCount - 1000);
updateParticleCount();
}
}
后期处理效果增强
添加屏幕空间效果提升视觉质量:
- 模糊处理:模拟光线散射
- 色彩分级:统一视觉风格
- 像素化滤镜:强化核心美学
// 后期处理片段着色器
uniform sampler2D uSceneTexture;
uniform vec2 uResolution;
uniform float uPixelSize;
void main() {
// 应用像素化
vec2 uv = gl_FragCoord.xy / uResolution;
uv = floor(uv / uPixelSize) * uPixelSize / uResolution;
// 采样场景
vec3 color = texture2D(uSceneTexture, uv).rgb;
// 添加CRT弯曲效果
vec2 crtUV = uv * 2.0 - 1.0;
float crtDistortion = 0.1;
crtUV *= 1.0 + crtDistortion * dot(crtUV, crtUV);
crtUV = (crtUV + 1.0) * 0.5;
if (crtUV.x < 0.0 || crtUV.x > 1.0 || crtUV.y < 0.0 || crtUV.y > 1.0) {
color = vec3(0.0);
} else {
color = texture2D(uSceneTexture, crtUV).rgb;
}
// 添加扫描线和噪点
float scanline = sin(gl_FragCoord.y * 3.1415) * 0.1 + 0.9;
float noise = fract(sin(dot(gl_FragCoord.xy)) * 43758.5453) * 0.1;
color = color * scanline + noise;
gl_FragColor = vec4(color, 1.0);
}
物理模拟的进阶技巧
更真实的蒸汽行为需要复杂物理模型:
- 湍流噪声:使用Perlin噪声创造有机运动
- 密度场:模拟蒸汽与空气的混合
- 温度梯度:影响上升速度和扩散模式
// 在JavaScript中生成Perlin噪声用于粒子运动
class PerlinNoise {
constructor() {
this.gradients = {};
this.memory = {};
}
randVector() {
const theta = Math.random() * Math.PI * 2;
return [Math.cos(theta), Math.sin(theta)];
}
dotGridGradient(ix, iy, x, y) {
let key = ix + ',' + iy;
if (!this.gradients[key]) {
this.gradients[key] = this.randVector();
}
const gradient = this.gradients[key];
const dx = x - ix;
const dy = y - iy;
return dx * gradient[0] + dy * gradient[1];
}
get(x, y) {
const x0 = Math.floor(x);
const x1 = x0 + 1;
const y0 = Math.floor(y);
const y1 = y0 + 1;
const sx = x - x0;
const sy = y - y0;
let n0 = this.dotGridGradient(x0, y0, x, y);
let n1 = this.dotGridGradient(x1, y0, x, y);
const ix0 = this.interpolate(n0, n1, sx);
n0 = this.dotGridGradient(x0, y1, x, y);
n1 = this.dotGridGradient(x1, y1, x, y);
const ix1 = this.interpolate(n0, n1, sx);
return this.interpolate(ix0, ix1, sy);
}
interpolate(a0, a1, w) {
return (a1 - a0) * (3.0 - w * 2.0) * w * w + a0;
}
}
// 在粒子更新中使用噪声
const perlin = new PerlinNoise();
function updateParticles() {
const time = Date.now() * 0.001;
for (let i = 0; i < particleCount; i++) {
const x = particles[i * 3];
const y = particles[i * 3 + 1];
const z = particles[i * 3 + 2];
// 使用3D噪声
const noiseX = perlin.get(x * 2, y * 2, time) * 0.02;
const noiseY = perlin.get(y * 2, time, z * 2) * 0.02;
const noiseZ = perlin.get(time, z * 2, x * 2) * 0.02;
particles[i * 3] += noiseX;
particles[i * 3 + 1] += noiseY + 0.01; // 基础上升速度
particles[i * 3 + 2] += noiseZ;
// 边界检查
if (particles[i * 3 + 1] > 1.0) {
particles[i * 3 + 1] = -1.0;
}
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn