WebGL Coffee Steam: 3D Particles and Temperature Simulation
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:
- Blocky Structures: Graphics composed of visibly distinct pixels or voxels
- Limited Color Palette: Often uses a reduced number of colors to enhance retro appeal
- 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:
- Particle Buffer Creation: Initialize particle data using
gl.createBuffer()
- Vertex Shader Processing: Compute particle positions in parallel on the GPU
- 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:
- Temperature Gradient Fields: Define temperature distribution in space
- Particle Response Functions: Adjust particle velocity based on local temperature
- 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:
- Mouse Interaction Hotspots: Detect user mouse position to influence temperature fields
- Touch Response: Support multi-touch on mobile devices
- 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:
- Instanced Rendering: Use
ANGLE_instanced_arrays
extension - Particle Pooling: Recycle invisible particles rather than creating new ones
- 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:
- Retro Game Style: Reduce particle count, enhance pixelation
- Realistic Style: Increase particle density, add lighting effects
- 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:
- WebGL Feature Detection: Check for extensions and limitations
- Fallback Solutions: Display static images when WebGL is unavailable
- 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:
- dat.GUI Integration: Create visual control panels
- URL Parameter Overrides: Allow parameter adjustments via query strings
- 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:
- Viewport Adaptation: Handle window size changes
- Resolution Scaling: Adjust based on device pixel ratio
- 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:
- Blur Processing: Simulate light scattering
- Color Grading: Unify visual style
- 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:
- Turbulence Noise: Use Perlin noise for organic motion
- Density Fields: Simulate steam-air mixing
- 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