셰이더로 사실적인 태양 만들기
이전 글1에서는 사실적인 지구를 만들었다. 고체 원소로 이루어진 지구와 달리, 태양은 기체로 가득 차 있다. 태양 표면을 흐르는 기체를 렌더링하기 위해, 여기2에서 언급한 프랙탈 노이즈, 즉 프랙탈 브라운 운동(fractal Brownian motion)을 활용한다. 또한 글로우(glow)와 프레넬(Fresnel) 효과도 렌더링한다.
이 글은 태양계 시뮬레이터 시리즈의 일부다:
- Three.js 좌표계 기초
- Three.js PBR (물리 기반 렌더링) 재질 기초
- 사실적인 지구 만들기
- 사실적인 태양 만들기
- 고리가 있는 행성 만들기
- 불규칙한 형태의 위성 만들기
- 태양을 빛나게 하기
- 은하수 스카이박스 만들기
- 타원 궤도 계산하기
표면
태양은 빛을 내는 유일한 천체이므로 낮과 밤이 없다. 오직 낮만 있을 뿐이다. 따라서 태양 표면의 텍스처만 디자인하면 된다. 이 글에서는 여기2에서 참고한 프랙탈 노이즈, 즉 프랙탈 브라운 운동을 이용해 태양의 표면을 렌더링한다. 3D 공간에서 프랙탈 노이즈를 렌더링하기 위해, 입력이 3D 벡터인 random 함수와 noise 함수를 미리 정의한다. 난수를 생성하는 다른 매직 넘버를 찾아 사용해도 된다.
// 2D Random
float random (in vec3 st) {
return fract(sin(dot(st,vec3(12.9898,78.233,23.112)))*12943.145);
}
한편, 3D 벡터를 입력으로 받는 noise 함수는 다음과 같다.
float noise (in vec3 _pos) {
vec3 i_pos = floor(_pos);
vec3 f_pos = fract(_pos);
// Four corners in 2D of a tile
float aa = random(i_pos);
float ab = random(i_pos + vec3(1., 0., 0.));
float ac = random(i_pos + vec3(0., 1., 0.));
float ad = random(i_pos + vec3(1., 1., 0.));
float ae = random(i_pos + vec3(0., 0., 1.));
float af = random(i_pos + vec3(1., 0., 1.));
float ag = random(i_pos + vec3(0., 1., 1.));
float ah = random(i_pos + vec3(1., 1., 1.));
// Smooth step
vec3 t = smoothstep(0., 1., f_pos);
// Mix 4 corners percentages
return
mix(
mix(
mix(aa,ab,t.x),
mix(ac,ad,t.x),
t.y),
mix(
mix(ae,af,t.x),
mix(ag,ah,t.x),
t.y),
t.z)
}
위 noise 함수는 3D 공간을 기반으로 하므로, x, y, z 축의 3차원 smoothing step 벡터 t로 총 여덟 개의 값을 섞는다.
나아가 시간에 따라 값을 변화시키기 위해, i_pos, f_pos와 유사하게 i_time과 f_time을 정의했다. 그러면 여덟 개의 새로운 값 ba ~ bh가 도입된다. 아래에서는 float aa = random(i_pos + 2.*i_time)처럼 위치 변수에 시간 변수를 더했다. 대신, vec4 타입에 대한 random 함수를 정의하고 float aa = random(vec4(i_pos, i_time))처럼 사용해도 된다.
float noise (in vec3 _pos) {
vec3 i_pos = floor(_pos);
vec3 f_pos = fract(_pos);
float i_time = floor(u_time*0.2);
float f_time = fract(u_time*0.2);
// Four corners in 2D of a tile
float aa = random(i_pos + i_time);
float ab = random(i_pos + i_time + vec3(1., 0., 0.));
float ac = random(i_pos + i_time + vec3(0., 1., 0.));
float ad = random(i_pos + i_time + vec3(1., 1., 0.));
float ae = random(i_pos + i_time + vec3(0., 0., 1.));
float af = random(i_pos + i_time + vec3(1., 0., 1.));
float ag = random(i_pos + i_time + vec3(0., 1., 1.));
float ah = random(i_pos + i_time + vec3(1., 1., 1.));
float ba = random(i_pos + (i_time + 1.));
float bb = random(i_pos + (i_time + 1.) + vec3(1., 0., 0.));
float bc = random(i_pos + (i_time + 1.) + vec3(0., 1., 0.));
float bd = random(i_pos + (i_time + 1.) + vec3(1., 1., 0.));
float be = random(i_pos + (i_time + 1.) + vec3(0., 0., 1.));
float bf = random(i_pos + (i_time + 1.) + vec3(1., 0., 1.));
float bg = random(i_pos + (i_time + 1.) + vec3(0., 1., 1.));
float bh = random(i_pos + (i_time + 1.) + vec3(1., 1., 1.));
// Smooth step
vec3 t = smoothstep(0., 1., f_pos);
float t_time = smoothstep(0., 1., f_time);
// Mix 4 corners percentages
return
mix(
mix(
mix(mix(aa,ab,t.x), mix(ac,ad,t.x), t.y),
mix(mix(ae,af,t.x), mix(ag,ah,t.x), t.y),
t.z),
mix(
mix(mix(ba,bb,t.x), mix(bc,bd,t.x), t.y),
mix(mix(be,bf,t.x), mix(bg,bh,t.x), t.y),
t.z),
t_time);
}
다음으로 위 noise 함수를 이용해 프랙탈 브라운 운동을 만든다.
#define NUM_OCTAVES 6
float fBm ( in vec3 _pos, in float sz) {
float v = 0.0;
float a = 0.2;
_pos *= sz;
vec3 angle = vec3(-0.001*u_time,0.0001*u_time,0.0004*u_time);
mat3 rotx = mat3(1, 0, 0,
0, cos(angle.x), -sin(angle.x),
0, sin(angle.x), cos(angle.x));
mat3 roty = mat3(cos(angle.y), 0, sin(angle.y),
0, 1, 0,
-sin(angle.y), 0, cos(angle.y));
mat3 rotz = mat3(cos(angle.z), -sin(angle.z), 0,
sin(angle.z), cos(angle.z), 0,
0, 0, 1);
for (int i = 0; i < NUM_OCTAVES; ++i) {
v += a * noise(_pos);
_pos = rotx * roty * rotz * _pos * 2.0;
a *= 0.8;
}
return v;
}
void main() {
vec3 st = vPosition;
vec3 q = vec3(0.);
q.x = fBm( st, 5.);
q.y = fBm( st + vec3(1.2,3.2,1.52), 5.);
q.z = fBm( st + vec3(0.02,0.12,0.152), 5.);
float n = fBm(st+q+vec3(1.82,1.32,1.09), 5.);
vec3 color = vec3(0.);
color = mix(vec3(1.,0.4,0.), vec3(1.,1.,1.), n*n);
color = mix(color, vec3(1.,0.,0.), q*0.7);
gl_FragColor = vec4(1.6*color, 1.);
프랙탈의 옥타브가 커짐에 따라 진폭과 각도를 직접 조정한다. 결과는 아래와 같다.

글로우
글로우(glow) 효과는 빛을 내는 물체를 더 밝아보이게 만들 수 있다. 글로우 효과는 물체 자체가 아니라 카메라에 나타나는 효과이므로, 카메라 좌표계에서 벡터 vNormalView를 정의한다. 참고로 아래 이미지들은 vNormal, vNormalModel, vNormalView의 차이를 보여준다. 보다시피 vNormal, vNormalModel, vNormalView는 각각 월드, 모델, 카메라 좌표계를 기준으로 정의된다.
vNormal |
vNormalModel |
vNormalView |
|---|---|---|
![]() |
![]() |
![]() |
Three.js에서 vNormal, vNormalModel, vNormalView는 미리 정의된 속성 normal, modelMatrix, normalMatrix로부터 유도할 수 있다.
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vNormalModel;
varying vec3 vNormalView;
varying vec3 vPosition;
void main() {
vUv = uv;
vNormal = normalize(mat3(modelMatrix) * normal);
vNormalModel = normal;
vNormalView = normalize(normalMatrix * normal);
vPosition = normalize(vec3(modelViewMatrix * vec4(position, 1.0)).xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
따라서 dot(vPosition, vNormalView)는 객체의 중심에서 가장 큰 값을 가지고, 경계에서는 작은 값을 가진다.
uniform vec3 u_color;
varying vec3 vPosition;
varying vec3 vNormalView;
void main() {
float raw_intensity = max(dot(vPosition, vNormalView), 0.);
float intensity = pow(raw_intensity, 4.);
vec4 color = vec4(u_color, intensity);
gl_FragColor = color;
}

프레넬
이전 글1에서 언급했듯이, 빛을 내는 객체를 렌더링할 때는 경계에서 입사각이 커지기 때문에 객체의 경계가 더 밝아 보인다는 점을 고려해야 한다. 아래에서 프레넬 효과는 fresnelTerm_outer 항으로 표현된다. 또한 객체 중심에서 나오는 빛은 카메라로 더 강하게 들어오므로, 이 효과는 fresnelTerm_inner 항으로 표현된다. 글로우 효과와 마찬가지로, 프레넬 효과를 렌더링하는 데 vNormalView를 사용했다.
uniform vec3 u_color;
varying vec3 vPosition;
varying vec3 vNormalView;
void main() {
float fresnelTerm_inner = 0.2 - 0.7*min(dot(vPosition, vNormalView), 0.0);
fresnelTerm_inner = pow(fresnelTerm_inner, 5.0);
float fresnelTerm_outer = 1.0 + dot(normalize(vPosition), normalize(vNormalView));
fresnelTerm_outer = pow(fresnelTerm_outer, 2.0);
float fresnelTerm = fresnelTerm_inner + fresnelTerm_outer;
gl_FragColor = vec4( u_color, 0.7 ) * fresnelTerm;
}



