阿里云主机折上折
  • 微信号
Current Site:Index > WebGL Coffee Steam: 3D Particles and Temperature Simulation

WebGL Coffee Steam: 3D Particles and Temperature Simulation

Author:Chuan Chen 阅读数:12476人阅读 分类: 前端综合

Pixelated Aesthetics in Visuals and Interaction: WebGL Coffee Steam - 3D Particles and Temperature Simulation

Pixelated aesthetics have always held a unique position in digital art, evoking nostalgia through low-resolution, blocky elements while combining with modern technology to create new visual languages. WebGL technology enables this aesthetic to be realized in browsers, particularly through 3D particles and physics simulations, allowing for the construction of delicate dynamic effects like coffee steam. The influence of temperature changes on particle behavior further enhances realism, making interactions more vivid.

Core Concepts of Pixelated Aesthetics

Pixelated aesthetics originated from the limitations of early computer graphics but have now become an intentionally pursued style. Its characteristics include:

  1. Blocky Structures: Graphics composed of visibly distinct pixels or voxels
  2. Limited Color Palette: Often uses a reduced number of colors to enhance retro appeal
  3. Jagged Edges: Deliberately retains stair-stepped edges rather than anti-aliasing
// Simple pixelation shader example  
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;  
}  
`;  

Implementing Particle Systems in WebGL

WebGL particle systems efficiently render large numbers of particles via vertex shaders, with each particle representing a tiny unit in the steam. Key implementation steps include:

  1. Particle Buffer Creation: Initialize particle data using gl.createBuffer()
  2. Vertex Shader Processing: Compute particle positions in parallel on the GPU
  3. Attribute Pointer Setup: Pass data via gl.vertexAttribPointer()
class ParticleSystem {  
    constructor(gl, count = 1000) {  
        this.gl = gl;  
        this.count = count;  
        this.particles = new Float32Array(count * 3);  
        
        // Initialize random particle positions  
        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() {  
        // Particle update logic  
        for (let i = 0; i < this.count; i++) {  
            // Simulate simple upward motion  
            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);  
    }  
}  

Influence of Temperature Fields on Particle Behavior

Temperature simulation adds physical realism to steam effects. This can be achieved through:

  1. Temperature Gradient Fields: Define temperature distribution in space
  2. Particle Response Functions: Adjust particle velocity based on local temperature
  3. Heat Conduction Simulation: Diffuse temperature changes over time
// Temperature influence in vertex shader  
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;  
    
    // Temperature affects particle rise speed and color  
    float riseSpeed = 0.1 * temperature;  
    vec3 position = aPosition + vec3(0.0, riseSpeed * uTime, 0.0);  
    
    // Map temperature to color (cool to warm)  
    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);  
}  

Interaction Design and User Experience Optimization

Thoughtful interaction design significantly enhances visual impact:

  1. Mouse Interaction Hotspots: Detect user mouse position to influence temperature fields
  2. Touch Response: Support multi-touch on mobile devices
  3. Performance Balancing: Dynamically adjust particle count based on device capability
// Interactive temperature field update  
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;  
    
    // Update temperature data in WebGL texture  
    updateTemperatureField(x, y, 0.5); // Add heat at mouse position  
});  

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);  
            
            // Gaussian heat source distribution  
            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);  
}  

Performance Optimization Techniques

Large-scale particle rendering requires special attention to performance:

  1. Instanced Rendering: Use ANGLE_instanced_arrays extension
  2. Particle Pooling: Recycle invisible particles rather than creating new ones
  3. LOD Systems: Adjust particle detail based on view distance
// Optimize performance with instanced rendering  
const ext = gl.getExtension('ANGLE_instanced_arrays');  
if (ext) {  
    // Set up instanced attributes  
    ext.vertexAttribDivisorANGLE(positionAttrLoc, 1);  
    ext.vertexAttribDivisorANGLE(colorAttrLoc, 1);  
    
    // Draw call  
    ext.drawArraysInstancedANGLE(gl.POINTS, 0, 1, particleCount);  
} else {  
    // Fallback to standard rendering  
    gl.drawArrays(gl.POINTS, 0, particleCount);  
}  

Visual Style Customization

Different steam styles can be created through parameter adjustments:

  1. Retro Game Style: Reduce particle count, enhance pixelation
  2. Realistic Style: Increase particle density, add lighting effects
  3. Artistic Expression: Use unnatural color mapping
// Stylized fragment shader  
varying vec3 vColor;  
uniform float uPixelation;  

void main() {  
    // Apply pixelation  
    vec2 uv = gl_FragCoord.xy / uPixelation;  
    uv = floor(uv) * uPixelation / uResolution;  
    
    // Limit color count for retro style  
    vec3 color = floor(vColor * 4.0) / 4.0;  
    
    // Add scanline effect  
    float scanline = mod(gl_FragCoord.y, 2.0) * 0.1 + 0.9;  
    color *= scanline;  
    
    gl_FragColor = vec4(color, 1.0);  
}  

Cross-Browser Compatibility Handling

Ensure consistent performance across platforms:

  1. WebGL Feature Detection: Check for extensions and limitations
  2. Fallback Solutions: Display static images when WebGL is unavailable
  3. Mobile Adaptation: Adjust touch interactions and render resolution
// Compatibility checks during WebGL initialization  
function initWebGL() {  
    try {  
        const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');  
        if (!gl) {  
            showFallbackImage();  
            return null;  
        }  
        
        // Check required extensions  
        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);  
}  

Dynamic Parameter Adjustment System

Adding real-time tuning tools accelerates development iteration:

  1. dat.GUI Integration: Create visual control panels
  2. URL Parameter Overrides: Allow parameter adjustments via query strings
  3. Preset Systems: Save and load different configurations
// Create control panel with 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);  
}  

Responsive Design and Adaptive Rendering

Ensure the effect displays well across devices:

  1. Viewport Adaptation: Handle window size changes
  2. Resolution Scaling: Adjust based on device pixel ratio
  3. Performance Adaptation: Dynamically degrade quality based on frame rate
// Responsive adjustments  
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);  
        
        // Adjust projection matrix  
        const aspect = width / height;  
        mat4.perspective(projectionMatrix, 45 * Math.PI / 180, aspect, 0.1, 100.0);  
        
        // May need to recreate textures and framebuffers  
        initFramebuffers();  
    }  
}, 200));  

// Performance adaptation  
let lastFrameTime = 0;  
function checkPerformance() {  
    const now = performance.now();  
    const deltaTime = now - lastFrameTime;  
    lastFrameTime = now;  
    
    // If frame rate drops below 30fps, reduce particle count  
    if (deltaTime > 33 && params.particleCount > 2000) {  
        params.particleCount = Math.max(2000, params.particleCount - 1000);  
        updateParticleCount();  
    }  
}  

Post-Processing Effects Enhancement

Add screen-space effects to improve visual quality:

  1. Blur Processing: Simulate light scattering
  2. Color Grading: Unify visual style
  3. Pixelation Filters: Strengthen core aesthetics
// Post-processing fragment shader  
uniform sampler2D uSceneTexture;  
uniform vec2 uResolution;  
uniform float uPixelSize;  

void main() {  
    // Apply pixelation  
    vec2 uv = gl_FragCoord.xy / uResolution;  
    uv = floor(uv / uPixelSize) * uPixelSize / uResolution;  
    
    // Sample scene  
    vec3 color = texture2D(uSceneTexture, uv).rgb;  
    
    // Add CRT curvature effect  
    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;  
    }  
    
    // Add scanlines and noise  
    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);  
}  

Advanced Techniques for Physics Simulation

More realistic steam behavior requires complex physical models:

  1. Turbulence Noise: Use Perlin noise for organic motion
  2. Density Fields: Simulate steam-air mixing
  3. Temperature Gradients: Influence rise speed and diffusion patterns
// Generate Perlin noise in JavaScript for particle motion  
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;  
    }  
}  

// Use noise in particle updates  
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];  
        
        // Use 3D noise  
        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; // Base rise speed  
        particles[i * 3 + 2] += noiseZ;  
        
        // Boundary check  
        if (particles[i * 3 + 

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.