Public
Edited
Jun 9, 2024
1 fork
Also listed in…
regl
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
numberOfParticles = 65536
Insert cell
canvas = {
const aspectRatio = 0.5; // 0.6
const c = DOM.canvas(
width * (devicePixelRatio || 1),
width * aspectRatio * (devicePixelRatio || 1)
);
c.style.width = `${width}px`;
c.style.height = `${width * aspectRatio}px`;
return c;
}
Insert cell
Insert cell
regl.frame(() => {
camera(() => {
regl.clear({ color: [0, 0.1, 0.26, 1] });
// regl.clear({ color: [0.01, 0.01, 0.01, 1] });

updateParticleTexture(); // 파티클의 상태(위치, 속도 등)를 업데이트
updateParticleScreen(); // 파티클이 화면에 어떻게 그려지는지를 업데이트

drawGlobe({ texScreen: screenTextures[0] }); // 지구 그리기
});
})
Insert cell
Insert cell
Insert cell
Insert cell
function updateParticleScreen() {
const backgroundTexture = screenTextures[0];
const screenTexture = screenTextures[1];

// 임시 프레임 버퍼에 화면을 그려 다음 프레임의 배경으로 사용
framebuffer({ color: screenTexture });

framebuffer.use(() => {
drawScreenTexture({
texture: backgroundTexture,
blend: false,
opacity: 0.95,
quadBuffer
});

drawParticleTexture({
particleStateResolution,
windMin: [windData.uMin, windData.vMin],
windMax: [windData.uMax, windData.vMax],
colorRampTexture,
particleStateTexture: particleStateTextures[0],
windTexture: regl.texture(texWind),
particleIndexBuffer
});
});

// 화면 텍스처를 교체
const temp = backgroundTexture;
screenTextures[0] = screenTexture;
screenTextures[1] = temp;
}
Insert cell
drawScreenTexture = regl({
frag: `
precision highp float;

uniform sampler2D u_screen;
uniform float u_opacity;

varying vec2 v_tex_pos;

void main() {
vec4 color = texture2D(u_screen, 1.0 - v_tex_pos);
// a hack to guarantee opacity fade out even with a value close to 1.0
gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0);
}`,

vert: `
precision mediump float;
attribute vec2 a_pos;
varying vec2 v_tex_pos;

void main() {
v_tex_pos = a_pos;
gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1);
}`,

blend: {
enable: regl.prop("blend"),
func: {
srcRGB: "src alpha",
srcAlpha: 1,
dstRGB: "one minus src alpha",
dstAlpha: 1
}
},

viewport: {
width: canvas.width,
height: canvas.height
},

depth: {
enable: false
},

stencil: {
enable: false
},

attributes: {
a_pos: regl.prop("quadBuffer")
},

uniforms: {
u_opacity: regl.prop("opacity"),
u_screen: regl.prop("texture")
},

primitive: "triangles",
count: 6
})
Insert cell
drawParticleTexture = regl({
primitive: "points",
count: numberOfParticles,

viewport: {
width: canvas.width,
height: canvas.height
},

depth: {
enable: false
},
stencil: {
enable: false
},

vert: `
precision highp float;
attribute float a_index;
uniform sampler2D u_particles;
uniform float u_particles_res;
varying vec2 v_particle_pos;
const vec2 bitEnc = vec2(1.,255.);
const vec2 bitDec = 1./bitEnc;
// decode particle position from pixel RGBA
vec2 fromRGBA(const vec4 color) {
vec4 rounded_color = floor(color * 255.0 + 0.5) / 255.0;
float x = dot(rounded_color.rg, bitDec);
float y = dot(rounded_color.ba, bitDec);
return vec2(x, y);
}
void main() {
vec4 color = texture2D(u_particles, vec2(
fract(a_index / u_particles_res),
floor(a_index / u_particles_res) / u_particles_res));
// decode current particle position from the pixel's RGBA value
v_particle_pos = fromRGBA(color);
gl_PointSize = 1.0;
gl_Position = vec4(2.0 * v_particle_pos.x - 1.0, 1.0 - 2.0 * v_particle_pos.y, 0, 1);
}`,

frag: `
precision highp float;

uniform sampler2D u_wind;
uniform vec2 u_wind_min;
uniform vec2 u_wind_max;
uniform sampler2D u_color_ramp;
varying vec2 v_particle_pos;
void main() {
vec2 velocity = mix(u_wind_min, u_wind_max, texture2D(u_wind, v_particle_pos).rg);
float speed_t = length(velocity) / length(u_wind_max);
// color ramp is encoded in a 16x16 texture
vec2 ramp_pos = vec2(
fract(16.0 * speed_t),
floor(16.0 * speed_t) / 16.0);
gl_FragColor = texture2D(u_color_ramp, ramp_pos);
}`,

attributes: {
a_index: regl.prop("particleIndexBuffer")
},

uniforms: {
u_wind: regl.prop("windTexture"),
u_particles: regl.prop("particleStateTexture"),
u_color_ramp: regl.prop("colorRampTexture"),

u_particles_res: regl.prop("particleStateResolution"),
u_wind_min: regl.prop("windMin"),
u_wind_max: regl.prop("windMax")
}
})
Insert cell
colorRampTexture = {
const getColorRamp = (colors) => {
const canvas = DOM.canvas(256, 1);
const ctx = canvas.getContext("2d");

const gradient = ctx.createLinearGradient(0, 0, 256, 0);
for (const stop in colors) {
gradient.addColorStop(+stop, colors[stop]);
}

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 256, 1);

return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data);
};
return regl.texture({
data: getColorRamp(rampColors),
width: 16,
height: 16,
mag: "linear",
min: "linear"
});
}
Insert cell
rampColors = ({
0.0: "#3288bd",
0.1: "#66c2a5",
0.2: "#abdda4",
0.3: "#e6f598",
0.4: "#fee08b",
0.5: "#fdae61",
0.6: "#f46d43",
1.0: "#d53e4f"
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function updateParticleTexture() {
framebuffer({ color: particleStateTextures[1] });

framebuffer.use(() => {
updateParticles({
quadBuffer,
windTexture: regl.texture(texWind),
particleStateTexture: particleStateTextures[0],
windRes: [windData.width, windData.height],
windMin: [windData.uMin, windData.vMin],
windMax: [windData.uMax, windData.vMax],
speedFactor: 0.25,
dropRate: 0.003,
dropRateBump: 0.01
});
});

// 파티클 상태 텍스처를 교체
const temp = particleStateTextures[0];
particleStateTextures[0] = particleStateTextures[1];
particleStateTextures[1] = temp;
}
Insert cell
updateParticles = regl({
frag: `
precision highp float;
uniform sampler2D u_particles;
uniform sampler2D u_wind;
uniform vec2 u_wind_res;
uniform vec2 u_wind_min;
uniform vec2 u_wind_max;
uniform float u_rand_seed;
uniform float u_speed_factor;
uniform float u_drop_rate;
uniform float u_drop_rate_bump;
varying vec2 v_tex_pos; // 정점 셰이더에서 전달된 텍스처 좌표
const vec2 bitEnc = vec2(1.,255.);
const vec2 bitDec = 1./bitEnc;
// decode particle position from pixel RGBA
// RGBA 색상을 좌표로 디코딩
vec2 fromRGBA(const vec4 color) {
vec4 rounded_color = floor(color * 255.0 + 0.5) / 255.0;
float x = dot(rounded_color.rg, bitDec);
float y = dot(rounded_color.ba, bitDec);
return vec2(x, y);
}
// encode particle position to pixel RGBA
// 좌표를 RGBA 색상으로 인코딩
vec4 toRGBA (const vec2 pos) {
vec2 rg = bitEnc * pos.x;
rg = fract(rg);
rg -= rg.yy * vec2(1. / 255., 0.);
vec2 ba = bitEnc * pos.y;
ba = fract(ba);
ba -= ba.yy * vec2(1. / 255., 0.);
return vec4(rg, ba);
}
// pseudo-random generator
// 난수 생성기 (텍스처 좌표 기반)
const vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453);
float rand(const vec2 co) {
float t = dot(rand_constants.xy, co);
return fract(sin(t) * (rand_constants.z + t));
}

// wind speed lookup; use manual bilinear filtering based on 4 adjacent pixels for smooth interpolation
// 바람 속도를 조회
vec2 lookup_wind(const vec2 uv) {
// return texture2D(u_wind, uv).rg; // lower-res hardware filtering
vec2 px = 1.0 / u_wind_res;
vec2 vc = (floor(uv * u_wind_res)) * px;
vec2 f = fract(uv * u_wind_res);
vec2 tl = texture2D(u_wind, vc).rg;
vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg;
vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg;
vec2 br = texture2D(u_wind, vc + px).rg;
return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);
}


void main() {
////////////// 현재 파티클의 상태(색상)를 가져와서 좌표로 변환 //////////////
vec4 color = texture2D(u_particles, v_tex_pos);
vec2 pos = fromRGBA(color);

// 바람 데이터를 기반으로 속도 계산
vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos));
float speed_t = length(velocity) / length(u_wind_max);
// take EPSG:4236 distortion into account for calculating where the particle moved
// update particle position, wrapping around the date line
// 파티클의 위치를 업데이트
float distortion = 1.0; //cos(radians(pos.y * 180.0 - 90.0));
vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor;
pos = fract(1.0 + pos + offset);
// a random seed to use for the particle drop
// 랜덤 시드를 사용하여 파티클 리셋 확률 계산
vec2 seed = (pos + v_tex_pos) * u_rand_seed;
// drop rate is a chance a particle will restart at random position, to avoid degeneration
float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump;
float drop = step(1.0 - drop_rate, rand(seed));


// 파티클을 랜덤 위치로 리셋
vec2 random_pos = vec2(
rand(seed + 1.3),
rand(seed + 2.1));
pos = mix(pos, random_pos, drop);
// encode the new particle position back into RGBA
// 새로운 파티클 위치를 RGBA 색상으로 인코딩
gl_FragColor = toRGBA(pos);
}
`,

vert: `
precision mediump float;
attribute vec2 a_pos;

// 프래그먼트 셰이더로 전달되는 텍스처 좌표
varying vec2 v_tex_pos;
void main() {
v_tex_pos = a_pos;
gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1);
}`,

primitive: "triangles",
count: 6,

viewport: {
width: particleStateResolution,
height: particleStateResolution
},

depth: {
enable: false
},
stencil: {
enable: false
},

attributes: {
a_pos: regl.prop("quadBuffer")
},

uniforms: {
u_wind: regl.prop("windTexture"),
u_particles: regl.prop("particleStateTexture"),

u_rand_seed: () => Math.random(),
u_wind_res: regl.prop("windRes"),
u_wind_min: regl.prop("windMin"),
u_wind_max: regl.prop("windMax"),
u_speed_factor: regl.prop("speedFactor"),
u_drop_rate: regl.prop("dropRate"),
u_drop_rate_bump: regl.prop("dropRateBump")
}
})
Insert cell
particleStateTextures = {
const particleState = new Uint8Array(numberOfParticles * 4)
.fill(null)
.map(() => Math.floor(Math.random() * 256)); // randomize the initial particle positions

return [
regl.texture({
data: particleState,
width: particleStateResolution,
height: particleStateResolution,
mag: "nearest",
min: "nearest"
}),

regl.texture({
data: particleState,
width: particleStateResolution,
height: particleStateResolution,
mag: "nearest",
min: "nearest"
})
];
}
Insert cell
Insert cell
particleStateResolution = Math.ceil(Math.sqrt(numberOfParticles))
Insert cell
// 각 파티클에 고유한 식별자를 부여하여 파티클 시스템 내에서 각 파티클을 개별적으로 처리가능
particleIndexBuffer = {
const particleIndices = new Float32Array(numberOfParticles);
for (let i = 0; i < numberOfParticles; i++) {
particleIndices[i] = i;
}
return regl.buffer(particleIndices);
}
Insert cell
// 프레임버퍼는 렌더링 결과를 화면이 아닌 텍스처에 저장할 때 사용
framebuffer = regl.framebuffer({
depthStencil: false,
colorCount: 1,
colorFormat: "rgba",
colorType: "uint8"
})
Insert cell
// 2D 좌표 배열을 Float32Array로
quadBuffer = regl.buffer(new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]))
Insert cell
Insert cell
drawGlobe = regl({
vert: `
precision mediump float;
attribute vec2 uv;
attribute vec3 position;

varying vec2 vertex;

uniform float tick;
uniform vec3 axis;
uniform mat4 projection, view;

const float PI = 3.14159;

// quatRotate 함수를 통해 회전을 계산하여 지구를 회전시킴 (텍스처, 회전축, tick)
vec3 quatRotate(vec3 v) {
float turn = -0.05 * tick * PI / 180.0;
vec4 q = vec4(axis * sin(turn), cos(turn));
return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
}

// 회전된 position을 클립 공간으로 변환하여 gl_Position에 설정
void main() {
vertex = uv.xy;
gl_Position = projection * view * vec4(quatRotate(position.xzy), 1.0);
}`,

frag: `
precision mediump float;
varying vec2 vertex;
uniform sampler2D tex, texScreen;

// tex (earth), texScreen (screen)
// 지구본이 회전할 때,
// 이전 프레임의 텍스처 (texScreen)와 현재 프레임의 지구 텍스처 (tex)를 혼합하여 자연스러운 시각적 효과를 만듦
void main() {
gl_FragColor = texture2D(tex, vertex) + texture2D(texScreen, vec2(vertex.x, 1.0 - vertex.y)) / 1.8;
}`,

attributes: {
position: sphereMesh.vertices, // 구의 버텍스 위치
uv: sphereMesh.uvs // 각 버텍스에 대한 텍스처 좌표
},

uniforms: {
tex: regl.texture(texEarth),
texScreen: regl.prop("texScreen"),
// tick: (ctx, props) => props.tick,
// tick: regl.prop("tick"),
tick: ({ tick }) => tick, // 렌더링된 프레임 수
axis: [0.0, 1.0, 0.0] // 회전축
},

elements: sphereMesh.faces
})
Insert cell
Insert cell
sphereMesh = sphereMeshFn(1, 30, 60) // (radius, nphi, ntheta)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
screenTextures = {
// 4는 각 픽셀이 RGBA(빨강, 초록, 파랑, 알파) 4개의 채널을 가지기 때문
// 이 배열은 모든 픽셀의 색상 데이터를 초기화하기 위한 것
const emptyPixels = new Uint8Array(canvas.width * canvas.height * 4);

return [
// Screen textures to hold the drawn screen for the previous and the current frame
regl.texture({
data: emptyPixels,
mag: "nearest",
min: "nearest",
width: canvas.width,
height: canvas.height
}),

regl.texture({
data: emptyPixels,
mag: "nearest",
min: "nearest",
width: canvas.width,
height: canvas.height
})
];
}
Insert cell
Insert cell
camera = reglCamera(regl, {
// element: canvas,
element: regl._gl.canvas, // regl의 WebGL 캔버스 요소를 사용
center: [0, 0, 0], // 카메라 중심위치
theta: -Math.PI / 2, // 카메라의 수평 회전각도
phi: Math.PI / 12, // 카메라의 수직 회전각도
distance: 3.0, // 카메라와 모델 사이의 거리
damping: 0.5, // 카메라 이동이 즉각적으로 반영됨
noScroll: true, // 스크롤로 인해 카메라가 이동하지 않도록
renderOnDirty: false, // 카메라가 변경될 때만 렌더링 수행하도록 설정하여 성능 최적화
zoomSpeed: 0
})
Insert cell
Insert cell
texEarth2 = FileAttachment("etopo-land_2.png").image({ width: 300 })
Insert cell
texEarth = FileAttachment("earth.png").image({ width: 300 })
Insert cell
texWind = FileAttachment("2020040100.png").image({ width: 300 })
Insert cell
windData = FileAttachment("2020040100.json").json()
Insert cell
Insert cell
reglCamera = (await import("https://cdn.skypack.dev/regl-camera@2.1.1")).default
Insert cell
regl = (await import("https://cdn.skypack.dev/regl@2")).default({
canvas
})
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more