阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > WebGL 咖啡蒸汽:3D 粒子与温度模拟

WebGL 咖啡蒸汽:3D 粒子与温度模拟

作者:陈川 阅读数:12475人阅读 分类: 前端综合

视觉与交互的“像素化美学” WebGL 咖啡蒸汽:3D 粒子与温度模拟

像素化美学在数字艺术中一直占据独特地位,它通过低分辨率、块状元素唤起复古情怀,同时与现代技术结合创造出新的视觉语言。WebGL 技术让这种美学在浏览器中得以实现,尤其是通过 3D 粒子和物理模拟,可以构建出如咖啡蒸汽般细腻的动态效果。温度变化对粒子行为的影响进一步增加了真实感,使交互更加生动。

像素化美学的核心概念

像素化美学源于早期计算机图形学的限制,但如今已成为一种刻意追求的风格。它的特点包括:

  1. 块状结构:图形由明显可见的像素或体素构成
  2. 有限调色板:通常使用减少的颜色数量增强复古感
  3. 锯齿边缘:故意保留阶梯状边缘而非抗锯齿处理
// 简单的像素化着色器示例
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 粒子系统通过顶点着色器高效渲染大量粒子,每个粒子可以代表蒸汽中的一个微小单元。关键实现步骤包括:

  1. 粒子缓冲区创建:使用 gl.createBuffer() 初始化粒子数据
  2. 顶点着色器处理:在 GPU 上并行计算粒子位置
  3. 属性指针设置:通过 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);
    }
}

温度场对粒子行为的影响

温度模拟为蒸汽效果增加了物理真实感。可以通过以下方式实现:

  1. 温度梯度场:定义空间中的温度分布
  2. 粒子响应函数:根据局部温度调整粒子速度
  3. 热传导模拟:随时间扩散温度变化
// 顶点着色器中的温度影响
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);
}

交互设计与用户体验优化

良好的交互设计能极大增强视觉效果的表现力:

  1. 鼠标交互热区:检测用户鼠标位置影响温度场
  2. 触摸响应:移动设备上的多点触控支持
  3. 性能平衡:根据设备能力动态调整粒子数量
// 交互式温度场更新
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);
}

性能优化技巧

大规模粒子渲染需要特别注意性能:

  1. 实例化渲染:使用 ANGLE_instanced_arrays 扩展
  2. 粒子池:复用不可见粒子而非创建新粒子
  3. 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);
}

视觉风格定制

通过参数调整可以创造不同风格的蒸汽效果:

  1. 复古游戏风格:减少粒子数量,增加像素感
  2. 写实风格:增加粒子密度,添加光照效果
  3. 艺术化表现:非自然色彩映射
// 风格化片段着色器
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);
}

跨浏览器兼容性处理

确保效果在不同平台一致表现:

  1. WebGL 特性检测:检查扩展和限制
  2. 回退方案:当WebGL不可用时显示静态图像
  3. 移动端适配:调整触摸交互和渲染分辨率
// 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);
}

动态参数调节系统

开发时添加实时调节工具能加速迭代:

  1. dat.GUI 集成:创建可视化控制面板
  2. URL参数覆盖:允许通过查询字符串调整参数
  3. 预设系统:保存和加载不同配置
// 使用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);
}

响应式设计与自适应渲染

确保效果在不同设备上都能良好展现:

  1. 视口适应:处理窗口大小变化
  2. 分辨率缩放:根据设备像素比例调整
  3. 性能自适应:基于帧率动态降级
// 响应式调整
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();
    }
}

后期处理效果增强

添加屏幕空间效果提升视觉质量:

  1. 模糊处理:模拟光线散射
  2. 色彩分级:统一视觉风格
  3. 像素化滤镜:强化核心美学
// 后期处理片段着色器
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);
}

物理模拟的进阶技巧

更真实的蒸汽行为需要复杂物理模型:

  1. 湍流噪声:使用Perlin噪声创造有机运动
  2. 密度场:模拟蒸汽与空气的混合
  3. 温度梯度:影响上升速度和扩散模式
// 在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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌