ivy-gpu-art-studio / js /particles.js
ijohn07's picture
Upload 21 files
d323047 verified
/**
* 🌿 Ivy's GPU Art Studio
* Tab 3: Particle Art
*
* GPU-computed particle systems with various behaviors
*/
class ParticlesRenderer {
constructor() {
this.device = null;
this.context = null;
this.format = null;
// Particle parameters
this.params = {
count: 10000,
mode: 0, // 0=attract, 1=repel, 2=orbit, 3=swarm, 4=ivy
size: 2.0,
speed: 1.0,
palette: 0, // 0=ivy, 1=rainbow, 2=fire, 3=ocean, 4=neon, 5=gold
trail: 0.1 // 0=no trail, higher=more trail
};
this.maxParticles = 100000;
this.input = null;
this.animationLoop = null;
this.isActive = false;
this.time = 0;
}
async init(device, context, format, canvas) {
this.device = device;
this.context = context;
this.format = format;
this.canvas = canvas;
await this.createBuffers();
await this.createPipelines();
this.input = new WebGPUUtils.InputHandler(canvas);
this.animationLoop = new WebGPUUtils.AnimationLoop((dt, time) => {
this.time = time;
this.simulate(dt);
this.render();
});
}
async createBuffers() {
// Particle positions (vec2) and velocities (vec2) = 16 bytes per particle
this.particleBuffer = this.device.createBuffer({
label: "Particle Buffer",
size: this.maxParticles * 16,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
});
// Uniform buffer
this.uniformBuffer = this.device.createBuffer({
label: "Particle Uniforms",
size: 64,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
// Initialize particles
this.respawnParticles();
}
respawnParticles() {
const data = new Float32Array(this.maxParticles * 4);
for (let i = 0; i < this.maxParticles; i++) {
const offset = i * 4;
// Random position
data[offset] = Math.random() * 2 - 1; // x
data[offset + 1] = Math.random() * 2 - 1; // y
// Random velocity
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 0.01;
data[offset + 2] = Math.cos(angle) * speed; // vx
data[offset + 3] = Math.sin(angle) * speed; // vy
}
this.device.queue.writeBuffer(this.particleBuffer, 0, data);
}
async createPipelines() {
// Compute shader
const computeShader = this.device.createShaderModule({
label: "Particle Compute Shader",
code: this.getComputeShaderCode()
});
// Render shader
const renderShader = this.device.createShaderModule({
label: "Particle Render Shader",
code: this.getRenderShaderCode()
});
// Bind group layout for compute
this.computeBindGroupLayout = this.device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
]
});
// Bind group layout for render
this.renderBindGroupLayout = this.device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
]
});
// Compute pipeline
this.computePipeline = this.device.createComputePipeline({
label: "Particle Compute Pipeline",
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.computeBindGroupLayout]
}),
compute: {
module: computeShader,
entryPoint: "main"
}
});
// Render pipeline
this.renderPipeline = this.device.createRenderPipeline({
label: "Particle Render Pipeline",
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.renderBindGroupLayout]
}),
vertex: {
module: renderShader,
entryPoint: "vertexMain",
buffers: [
{
arrayStride: 16, // vec4f (pos.xy, vel.xy)
stepMode: "instance",
attributes: [
{ shaderLocation: 0, offset: 0, format: "float32x2" }, // position
{ shaderLocation: 1, offset: 8, format: "float32x2" } // velocity
]
}
]
},
fragment: {
module: renderShader,
entryPoint: "fragmentMain",
targets: [
{
format: this.format,
blend: {
color: {
srcFactor: "src-alpha",
dstFactor: "one",
operation: "add"
},
alpha: {
srcFactor: "one",
dstFactor: "one",
operation: "add"
}
}
}
]
},
primitive: {
topology: "triangle-list"
}
});
// Create bind groups
this.computeBindGroup = this.device.createBindGroup({
layout: this.computeBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.uniformBuffer } },
{ binding: 1, resource: { buffer: this.particleBuffer } }
]
});
this.renderBindGroup = this.device.createBindGroup({
layout: this.renderBindGroupLayout,
entries: [{ binding: 0, resource: { buffer: this.uniformBuffer } }]
});
}
start() {
this.isActive = true;
this.animationLoop.start();
}
stop() {
this.isActive = false;
this.animationLoop.stop();
}
reset() {
this.respawnParticles();
}
setCount(count) {
this.params.count = Math.min(count, this.maxParticles);
}
setMode(mode) {
const modes = {
attract: 0,
repel: 1,
orbit: 2,
swarm: 3,
ivy: 4,
tunnel: 5, // Wormhole tunnel
dna: 6, // DNA helix sparkle
galaxy: 7, // Galaxy vortex
wavegrid: 8, // NEW: Wave grid (image 2)
splatter: 9 // NEW: Paint splatter clusters (image 3)
};
this.params.mode = modes[mode] || 0;
// Respawn particles for special modes
if (mode === "tunnel" || mode === "dna" || mode === "galaxy") {
this.respawnParticles3D();
} else if (mode === "wavegrid") {
this.respawnParticlesGrid();
} else if (mode === "splatter") {
this.respawnParticlesClusters();
}
}
// Grid spawn for wave effect
respawnParticlesGrid() {
const data = new Float32Array(this.maxParticles * 4);
const gridSize = Math.floor(Math.sqrt(this.maxParticles));
for (let i = 0; i < this.maxParticles; i++) {
const offset = i * 4;
const gx = (i % gridSize) / gridSize;
const gy = Math.floor(i / gridSize) / gridSize;
// Grid position (-1 to 1)
data[offset] = gx * 2 - 1; // x
data[offset + 1] = gy * 2 - 1; // y
data[offset + 2] = gx; // store grid coord for coloring
data[offset + 3] = gy;
}
this.device.queue.writeBuffer(this.particleBuffer, 0, data);
}
// Cluster spawn for paint splatter effect
respawnParticlesClusters() {
const data = new Float32Array(this.maxParticles * 4);
const numClusters = 30 + Math.floor(Math.random() * 20);
const clusters = [];
// Create cluster centers with random colors
for (let c = 0; c < numClusters; c++) {
clusters.push({
x: Math.random() * 2 - 1,
y: Math.random() * 2 - 1,
size: 0.1 + Math.random() * 0.25,
hue: Math.random() // Color identifier
});
}
for (let i = 0; i < this.maxParticles; i++) {
const offset = i * 4;
// Pick a random cluster
const cluster = clusters[Math.floor(Math.random() * numClusters)];
// Spawn within cluster with gaussian-like distribution
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * Math.random() * cluster.size; // Squared for density at center
data[offset] = cluster.x + Math.cos(angle) * dist; // x
data[offset + 1] = cluster.y + Math.sin(angle) * dist; // y
data[offset + 2] = cluster.hue; // store hue for color
data[offset + 3] = dist / cluster.size; // distance from center for variation
}
this.device.queue.writeBuffer(this.particleBuffer, 0, data);
}
// Special respawn for 3D-like effects
respawnParticles3D() {
const data = new Float32Array(this.maxParticles * 4);
for (let i = 0; i < this.maxParticles; i++) {
const offset = i * 4;
// Spawn in a cylinder/tube shape for better 3D effect
const angle = Math.random() * Math.PI * 2;
const radius = 0.3 + Math.random() * 0.7;
const z = Math.random() * 2 - 1; // Pseudo-depth
data[offset] = Math.cos(angle) * radius; // x
data[offset + 1] = Math.sin(angle) * radius; // y
data[offset + 2] = z * 0.01; // vx (store z as velocity for shader)
data[offset + 3] = (Math.random() - 0.5) * 0.01; // vy
}
this.device.queue.writeBuffer(this.particleBuffer, 0, data);
}
setSize(size) {
this.params.size = size;
}
setSpeed(speed) {
this.params.speed = speed;
}
setPalette(palette) {
const palettes = { ivy: 0, rainbow: 1, fire: 2, ocean: 3, neon: 4, gold: 5, cosmic: 6 };
this.params.palette = palettes[palette] ?? 0;
}
setTrail(trail) {
this.params.trail = trail;
}
simulate(dt) {
if (!this.isActive) return;
const aspect = this.canvas.width / this.canvas.height;
// Update uniforms
const uniforms = new Float32Array([
this.params.count,
dt * this.params.speed,
this.params.mode,
this.params.size,
this.input.mouseX * 2 - 1, // Normalized to -1..1
this.input.mouseY * 2 - 1,
this.input.isPressed ? 1.0 : 0.0,
this.time,
aspect,
this.params.palette,
this.params.trail,
0.0 // padding
]);
this.device.queue.writeBuffer(this.uniformBuffer, 0, uniforms);
// Run compute shader
const commandEncoder = this.device.createCommandEncoder();
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(this.computePipeline);
computePass.setBindGroup(0, this.computeBindGroup);
computePass.dispatchWorkgroups(Math.ceil(this.params.count / 64));
computePass.end();
this.device.queue.submit([commandEncoder.finish()]);
}
render() {
if (!this.isActive) return;
WebGPUUtils.resizeCanvasToDisplaySize(this.canvas, window.devicePixelRatio);
// Trail effect: use semi-transparent clear based on trail value
// Lower alpha = more trail persistence
// Clamped to prevent values below 0.05 which would cause issues
const trailAlpha = Math.max(0.05, 1.0 - this.params.trail * 1.9); // Better trail range
const commandEncoder = this.device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: this.context.getCurrentTexture().createView(),
clearValue: { r: 0.02 * trailAlpha, g: 0.02 * trailAlpha, b: 0.05 * trailAlpha, a: trailAlpha },
loadOp: "clear",
storeOp: "store"
}
]
});
renderPass.setPipeline(this.renderPipeline);
renderPass.setBindGroup(0, this.renderBindGroup);
renderPass.setVertexBuffer(0, this.particleBuffer);
renderPass.draw(6, this.params.count); // 6 vertices per quad, instanced
renderPass.end();
this.device.queue.submit([commandEncoder.finish()]);
}
getComputeShaderCode() {
return /* wgsl */ `
struct Uniforms {
count: f32,
dt: f32,
mode: f32,
size: f32,
mouseX: f32,
mouseY: f32,
mousePressed: f32,
time: f32,
aspect: f32,
palette: f32,
trail: f32,
}
struct Particle {
pos: vec2f,
vel: vec2f,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read_write> particles: array<Particle>;
// Simple hash function for randomness
fn hash(p: vec2f) -> f32 {
var h = dot(p, vec2f(127.1, 311.7));
return fract(sin(h) * 43758.5453123);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let idx = gid.x;
if (idx >= u32(u.count)) {
return;
}
var p = particles[idx];
let mouse = vec2f(u.mouseX, u.mouseY);
// Calculate force based on mode
var force = vec2f(0.0, 0.0);
let toMouse = mouse - p.pos;
let dist = length(toMouse);
let dir = normalize(toMouse + vec2f(0.0001, 0.0001));
let mode = i32(u.mode);
if (mode == 0) {
// Attract to mouse
if (u.mousePressed > 0.5 && dist > 0.01) {
force = dir * 0.5 / (dist * dist + 0.1);
}
} else if (mode == 1) {
// Repel from mouse
if (u.mousePressed > 0.5 && dist > 0.01) {
force = -dir * 0.5 / (dist * dist + 0.1);
}
} else if (mode == 2) {
// Orbit around mouse
if (dist > 0.01) {
let perpendicular = vec2f(-dir.y, dir.x);
force = perpendicular * 0.2 / (dist + 0.1);
force += dir * (0.5 - dist) * 0.1; // Pull toward orbit radius
}
} else if (mode == 3) {
// Swarm behavior
let noise = hash(p.pos + vec2f(u.time * 0.1, 0.0));
let angle = noise * 6.28318 + u.time;
force = vec2f(cos(angle), sin(angle)) * 0.05;
if (u.mousePressed > 0.5 && dist < 0.3) {
force += dir * 0.3;
}
} else if (mode == 4) {
// 🌿 Ivy mode - Falling leaves that grow/spiral like ivy
let noise = hash(p.pos + vec2f(f32(idx) * 0.01, 0.0));
// Gentle falling
force.y = -0.02;
// Swaying left-right like leaves in wind
let swayFreq = noise * 2.0 + 1.0;
let swayAmp = 0.03 + noise * 0.02;
force.x = sin(u.time * swayFreq + p.pos.y * 3.0 + noise * 6.28) * swayAmp;
// Spiral pattern (like ivy growing)
let spiralAngle = u.time * 0.5 + p.pos.y * 5.0 + noise * 6.28;
force.x += cos(spiralAngle) * 0.01;
// Mouse interaction - leaves follow cursor
if (u.mousePressed > 0.5) {
force += dir * 0.2 / (dist + 0.2);
} else if (dist < 0.3) {
// Gentle attract even without click
force += dir * 0.05 / (dist + 0.1);
}
} else if (mode == 5) {
// 🌀 WORMHOLE TUNNEL - Particles fly toward center creating tunnel effect
let noise = hash(p.pos + vec2f(f32(idx) * 0.01, 0.0));
// Distance from center
let centerDist = length(p.pos);
// Spiral inward
let angle = atan2(p.pos.y, p.pos.x);
let spiralSpeed = 0.1 + noise * 0.1;
let newAngle = angle + u.time * spiralSpeed;
// Pull toward center (tunnel effect)
let pullStrength = 0.05 * (1.0 + centerDist);
force = -normalize(p.pos + vec2f(0.001)) * pullStrength;
// Add rotation
let tangent = vec2f(-p.pos.y, p.pos.x);
force += normalize(tangent) * 0.1;
// When very close to center, respawn at edge
if (centerDist < 0.05) {
let spawnAngle = noise * 6.28318 + u.time;
p.pos = vec2f(cos(spawnAngle), sin(spawnAngle)) * (0.9 + noise * 0.2);
p.vel = vec2f(0.0);
}
} else if (mode == 6) {
// 🧬 DNA HELIX - Double helix sparkle spiral
let noise = hash(p.pos + vec2f(f32(idx) * 0.01, 0.0));
let particlePhase = f32(idx) / u.count;
// Two helices (DNA strands)
let strand = select(0.0, 3.14159, f32(idx % 2u) > 0.5);
let helixAngle = particlePhase * 20.0 + u.time * 2.0 + strand;
let helixRadius = 0.3 + 0.1 * sin(particlePhase * 10.0);
// Target position on helix
let targetX = cos(helixAngle) * helixRadius;
let targetY = (particlePhase * 2.0 - 1.0); // Vertical spread
let destPos = vec2f(targetX, targetY);
// Move toward helix position
force = (destPos - p.pos) * 0.1;
// Add sparkle jitter
force.x += sin(u.time * 10.0 + noise * 100.0) * 0.01;
force.y += cos(u.time * 8.0 + noise * 50.0) * 0.01;
// Mouse interaction - expand helix
if (u.mousePressed > 0.5) {
let expand = dir * 0.1;
force += expand;
}
} else if (mode == 7) {
// 🌌 GALAXY VORTEX - Spiral galaxy with arms
let noise = hash(p.pos + vec2f(f32(idx) * 0.01, 0.0));
let particlePhase = f32(idx) / u.count;
// Galaxy arm assignment (4 arms)
let armIndex = f32(idx % 4u);
let armOffset = armIndex * 1.5708; // PI/2
// Spiral formula
let spiralAngle = particlePhase * 15.0 + u.time * 0.5 + armOffset;
let spiralRadius = particlePhase * 0.8 + 0.1;
// Add arm spread
let spread = noise * 0.15;
let targetX = cos(spiralAngle) * (spiralRadius + spread);
let targetY = sin(spiralAngle) * (spiralRadius + spread);
let destPos = vec2f(targetX, targetY);
// Smooth movement toward spiral position
force = (destPos - p.pos) * 0.05;
// Orbital velocity (rotation)
let tangent = vec2f(-p.pos.y, p.pos.x);
force += normalize(tangent + vec2f(0.001)) * 0.02;
// Mouse creates gravity well
if (u.mousePressed > 0.5 && dist < 0.5) {
force += dir * 0.3 / (dist + 0.1);
}
} else if (mode == 8) {
// 🌊 WAVE GRID - Particles on grid with color waves passing through
// Grid particles don't move much - the color does the work
// Just subtle oscillation
let gridX = p.vel.x; // We stored grid coords in vel
let gridY = p.vel.y;
// Subtle wave movement
let waveX = sin(gridY * 10.0 + u.time * 2.0) * 0.005;
let waveY = cos(gridX * 10.0 + u.time * 1.5) * 0.005;
// Restore to grid position with wave offset
let targetX = (gridX * 2.0 - 1.0) + waveX;
let targetY = (gridY * 2.0 - 1.0) + waveY;
force = (vec2f(targetX, targetY) - p.pos) * 0.5;
// Mouse interaction - push particles away
if (dist < 0.2) {
force -= dir * 0.05 / (dist + 0.05);
}
} else if (mode == 9) {
// 🎨 PAINT SPLATTER - Clustered particles, minimal movement
// The beauty is in the static clusters, so minimal force
let noise = hash(p.pos + vec2f(f32(idx) * 0.01, u.time * 0.01));
// Very subtle jitter to keep them alive
force.x = sin(u.time * 3.0 + noise * 100.0) * 0.002;
force.y = cos(u.time * 2.5 + noise * 50.0) * 0.002;
// Mouse click explodes nearby clusters
if (u.mousePressed > 0.5 && dist < 0.3) {
force -= dir * 0.2 / (dist + 0.05);
}
}
// Apply force
p.vel += force * u.dt;
// Damping
p.vel *= 0.99;
// Limit speed
let speed = length(p.vel);
if (speed > 0.1) {
p.vel = normalize(p.vel) * 0.1;
}
// Update position
p.pos += p.vel * u.dt * 10.0;
// Wrap around edges
if (p.pos.x < -1.1) { p.pos.x = 1.1; }
if (p.pos.x > 1.1) { p.pos.x = -1.1; }
if (p.pos.y < -1.1) { p.pos.y = 1.1; }
if (p.pos.y > 1.1) { p.pos.y = -1.1; }
particles[idx] = p;
}
`;
}
getRenderShaderCode() {
return /* wgsl */ `
struct Uniforms {
count: f32,
dt: f32,
mode: f32,
size: f32,
mouseX: f32,
mouseY: f32,
mousePressed: f32,
time: f32,
aspect: f32,
palette: f32,
trail: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VertexInput {
@builtin(vertex_index) vertexIndex: u32,
@builtin(instance_index) instanceIndex: u32,
@location(0) pos: vec2f,
@location(1) vel: vec2f,
}
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
@location(1) speed: f32,
@location(2) vel: vec2f,
}
fn getPaletteColor(t: f32, paletteId: i32) -> vec3f {
let tt = fract(t);
if (paletteId == 0) { // Ivy Green
return vec3f(0.13 + 0.2 * tt, 0.5 + 0.4 * tt, 0.2 + 0.2 * tt);
} else if (paletteId == 1) { // Rainbow
return vec3f(
0.5 + 0.5 * cos(6.28318 * (tt + 0.0)),
0.5 + 0.5 * cos(6.28318 * (tt + 0.33)),
0.5 + 0.5 * cos(6.28318 * (tt + 0.67))
);
} else if (paletteId == 2) { // Fire
return vec3f(1.0, 0.3 + 0.5 * tt, tt * 0.2);
} else if (paletteId == 3) { // Ocean
return vec3f(0.1 * tt, 0.3 + 0.4 * tt, 0.6 + 0.4 * tt);
} else if (paletteId == 4) { // Neon
return vec3f(
0.5 + 0.5 * sin(tt * 12.0),
0.5 + 0.5 * sin(tt * 12.0 + 2.0),
0.5 + 0.5 * sin(tt * 12.0 + 4.0)
);
} else if (paletteId == 5) { // Gold
return vec3f(1.0, 0.8 * tt + 0.2, 0.2 * tt);
} else { // Cosmic - Purple/Pink/Blue sparkle like image 2
return vec3f(
0.4 + 0.6 * sin(tt * 8.0 + 1.0),
0.2 + 0.3 * sin(tt * 6.0 + 2.0),
0.6 + 0.4 * sin(tt * 10.0 + 4.0)
);
}
}
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
// Quad vertices
var quadPos = array<vec2f, 6>(
vec2f(-1.0, -1.0),
vec2f(1.0, -1.0),
vec2f(1.0, 1.0),
vec2f(-1.0, -1.0),
vec2f(1.0, 1.0),
vec2f(-1.0, 1.0)
);
let size = u.size * 0.01;
let offset = quadPos[input.vertexIndex] * size;
var output: VertexOutput;
output.position = vec4f(
input.pos.x + offset.x / u.aspect,
input.pos.y + offset.y,
0.0, 1.0
);
output.uv = quadPos[input.vertexIndex] * 0.5 + 0.5;
output.speed = length(input.vel);
output.vel = input.vel; // Pass velocity for special modes
return output;
}
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
// Circular particle with glow
let dist = length(input.uv - 0.5) * 2.0;
if (dist > 1.0) {
discard;
}
let paletteId = i32(u.palette);
let mode = i32(u.mode);
var hue = fract(input.speed * 20.0 + u.time * 0.1);
var color = getPaletteColor(hue, paletteId);
var alpha: f32;
if (mode == 8) {
// 🌊 WAVE GRID - Color based on wave function, not speed
let gridX = input.vel.x;
let gridY = input.vel.y;
// Multiple wave layers for color
let wave1 = sin(gridX * 8.0 + u.time * 1.5) * 0.5 + 0.5;
let wave2 = sin(gridY * 6.0 - u.time * 1.2) * 0.5 + 0.5;
let wave3 = sin((gridX + gridY) * 5.0 + u.time) * 0.5 + 0.5;
hue = fract(wave1 * 0.4 + wave2 * 0.3 + wave3 * 0.3);
color = getPaletteColor(hue, paletteId);
// Small dots with soft glow
let core = 1.0 - smoothstep(0.0, 0.4, dist);
let glow = 1.0 - smoothstep(0.0, 1.0, dist);
alpha = core * 0.95 + glow * 0.3;
color *= 1.2;
} else if (mode == 9) {
// 🎨 PAINT SPLATTER - Color based on cluster hue stored in vel.x
let clusterHue = input.vel.x;
let distFromCenter = input.vel.y;
// Vibrant distinct colors per cluster
hue = clusterHue;
color = getPaletteColor(hue, 1); // Force rainbow for best splatter effect
// Vary brightness based on distance from cluster center
let brightness = 0.8 + distFromCenter * 0.4;
color *= brightness;
// Solid dots with slight soft edge
alpha = 1.0 - smoothstep(0.7, 1.0, dist);
} else if (mode >= 5 && mode <= 7) {
// Enhanced glow for tunnel, dna, galaxy
let core = 1.0 - smoothstep(0.0, 0.3, dist);
let glow = 1.0 - smoothstep(0.0, 1.0, dist);
alpha = core * 0.9 + glow * 0.4;
let sparkle = sin(u.time * 20.0 + input.speed * 100.0) * 0.3 + 0.7;
color *= sparkle * 1.5;
} else {
// Standard soft edge for other modes
alpha = 1.0 - smoothstep(0.5, 1.0, dist);
}
return vec4f(color * alpha * 0.8, alpha * 0.6);
}
`;
}
}
// Export
window.ParticlesRenderer = ParticlesRenderer;