[Three.js] Create a Realistic Earth with Shaders

Let’s return to creating a realistic Earth using Three.js. Unlike the previous Earth1, we are going to render Earth using shader material. First, we will describe the day and night with two different textures, since there are city lights at night. Second, I’ll make an effect for mountain shadow and ocean reflection to make texture more realistic. Also, I’ll depict the flowing cloud and its shadow. Third, a high-quality eclipse shadow will be cast on the surface of Earth. At last, I’ll render the atmosphere and its Fresnel effect.

Day and night

In the previous article1, the day and night of Earth are generated by the brightness only. It would be fine if there is no civilization. However, at present, the city lights at night are so bright enough to cause light pollution. To describe the city lights, we use a night texture, u_nightTexture. In Three.js, a texture map can be passed to a shader using uniform variable. Refer to the article2.

const material = new THREE.ShaderMaterial({
    uniforms: {
        u_dayTexture: { value: new THREE.TextureLoader().load( PATH_TO_DAY_IMAGE) },
        u_nightTexture: { value: new THREE.TextureLoader().load( PATH_TO_NIGHT_IMAGE) },
    },
})

On the other hands, the texture map can be declared as

uniform sampler2D u_dayTexture;
uniform sampler2D u_nightTexture;

Below are the example of day and night map of Earth.

Then, RGB values of texture can be accessed by UV position, vUv, that is an attribute provided by Three.js.

vec3 dayColor = texture2D( u_dayTexture, vUv ).rgb;
vec3 nightColor = texture2D( u_nightTexture, vUv ).rgb;

To mix two textures, we have to compute a weight between them, and it depends on the direction of the Sun. Thus, we should let shaders know the relative position of Sun with respect to Earth, u_sunRelPosition.

uniform vec3 u_sunRelPosition; // the relative position of light source

void main( void ) {
    vec3 sunDir = normalize(u_sunRelPosition);
    ...
}

Then the dot product between the sun direction and the normal vector of the Earth surfaces denotes an incidence angle of sunlight which defines day or night.

float cosAngleSunToNormal = dot(vNormal, sunDir); // Compute cosine sun to normal
float mixAmountTexture = 1. / (1. + exp(-20. * cosAngleSunToNormal)); // Convert [+1, -1] -> [1, 0] with high contrast
    
// Combine night and day colors
vec3 color = mix(nightColor, dayColor, mixAmountTexture); // Select day or night texture

Seen from above, mixAmountTexture not only converts value from [+1, -1] to [1, 0], but also increases contrast through an exponential function for natural shading. Then, mix() function mixes the night color and day color. These basic functions are briefly explained before3.

Up to here, the result is below:

uniform sampler2D u_dayTexture;
uniform sampler2D u_nightTexture;
uniform vec3 u_sunRelPosition; // the relative position of light source

varying vec2 vUv; // texture UV map
varying vec3 vNormal; // normal vector at surface

void main( void ) {
    vec3 sunDir = normalize(u_sunRelPosition);

    // Day and night texture with eclipse
    vec3 dayColor = texture2D( u_dayTexture, vUv ).rgb;
    vec3 nightColor = texture2D( u_nightTexture, vUv ).rgb;

    float cosAngleSunToNormal = dot(vNormal, sunDir); // Compute cosine sun to normal
    float mixAmountTexture = 1. / (1. + exp(-20. * cosAngleSunToNormal));
    
    // Combine night and day colors
    vec3 color = mix( nightColor, dayColor, mixAmountTexture ); // Select day or night texture

    gl_FragColor = vec4(color, 1.);
}

Mountain shadow

Because our Earth model has been made of an ideal sphere, the surface of Earth looks so smooth. But, in reality, mountains casts complicated shadows on the surface, and the shadow representation of bumpy geography adds realism to the texture. For this representation, we use a normal map texture, u_normalTexture.

As mentioned in the previous article1, a single RGB value of a normal map is defined as below:

\[R = (n_x + 1) / 2,\] \[G = (n_y + 1) / 2,\] \[B = (n_z + 1) / 2.\]

Thus, the element of the normal vector is

\[n_x = 2 * R - 1,\] \[n_y = 2 * G - 1,\] \[n_z = 2 * B - 1.\]

In shader, we use

vec3 t_normal = texture2D( u_normalTexture, vUv ).xyz * 2.0 - 1.0;

The normal vector of texture2D is based on the local coordinate system of the object. Thus, in order to convert a normal vector from local coordinates to world coordinates, we multiply the normal vector by vTbn matrix. Each column of vTbn matrix is composed of tangent, normal, and the cross vector of both vectors with respect to world coordinates, and they can be computed from the attributes provided by Three.js. Therefore, a normal vector in world coordinate system is

vec3 normal = normalize(vTbn * t_normal);

Then, the angle between the normal vector and the direction of sunlight is

float cosAngleSunToSurface = dot(normal, sunDirUnit); // Compute cosine sun to normal

Finally, we add the difference between cosAngleSunToSurface and cosAngleSunToNormal into the calculation of mixAmountTexture. The below is the total code.

// Normal map texture
vec3 t_normal = texture2D( u_normalTexture, vUv ).xyz * 2.0 - 1.0;
vec3 normal = normalize(vTbn * t_normal);
float cosAngleSunToSurface = dot(normal, sunDir); // Compute cosine sun to normal
mixAmountTexture *= 1.0 + u_normalPower * (cosAngleSunToSurface - cosAngleSunToNormal);
mixAmountTexture = clamp(mixAmountTexture, 0., 1.);

Ocean reflection

To compute reflection and its effect on the camera, we need a specular map and the position of the camera. The example of the specular map is as follows.

To soften the effect of glitter, I’ve scaled down the value.

float reflectRatio = texture2D(u_specTexture, vUv).r;
reflectRatio = 0.3 * reflectRatio + 0.1; // [0, 1] -> [0.1, 0.4]

Then, the vector of sunlight reflecting at a surface is computed by

vec3 reflectVec = -reflect(sunDir, normal); // reflected vector of sunlight

, where reflect(v, w) = \(v - 2 * (v \cdot w) * w\).

Eventually, surface points whose reflection vectors are close to the vector starting at the camera position and ending at the surface position should be brightened. Notice that cameraPosition is a built-in attribute of Three.js, and denotes the position of the camera in world coordinates.

float specPower = dot(reflectVec, normalize(cameraPosition - surfacePosition)); // dot product between reflected light and camera vector. When they are close to each other, the result of dot product increases.
color += mixAmountTexture * pow(specPower, 2.0) * reflectRatio;

The part of specular map texture is summarized as below.

// Specular map texture with reflection
vec3 surfacePosition = u_position + vPosition; // surface position in world coordinates
float reflectRatio = texture2D(u_specTexture, vUv).r;
reflectRatio = 0.3 * reflectRatio + 0.1;
vec3 reflectVec = reflect(-sunDir, normal); // reflected vector of sunlight
float specPower = clamp(dot(reflectVec, normalize(cameraPosition - surfacePosition)), 0., 1.); // dot product between reflected light and camera vector
color += mixAmountTexture * pow(specPower, 2.0) * reflectRatio;

Cloud

On Earth, there are clouds due to the circulation of atmosphere. You can find cloud texture map in online.

In this section, we will render cloud and its shadow.

Since the cloud layer is on the top, you can just mix the previous texture and cloud texture. The above PNG file provides RGBA values, and we can use the value of alpha channel as a weight for the mix function.

vec4 cloudsColor = texture2D(u_cloudTexture, vUv);
cloudsColor *= clamp(mixAmountHemisphere, 0.2, 1.);
color = color * (1.0 - cloudsColor.a) + cloudsColor.rgb * cloudsColor.a;

Please notice that I’ve used a new value mixAmountHemisphere rather than mixAmountTexture to implement a day-and-night of cloud, because mixAmountTexture includes the shadow of mountains.

float mixAmountHemisphere = 1. / (1. + exp(-20. * cosAngleSunToNormal));

To render more realistic cloud, we can independently adjust RGB value of cloudColor, for example, making red color diminish gradually while blue color become dark sharply, at the edge of day and night.

cloudsColor.r *= clamp(mixAmountHemisphere, 0.2, 1.);
cloudsColor.g *= clamp(pow(mixAmountHemisphere, 1.5), 0.2, 1.);
cloudsColor.b *= clamp(pow(mixAmountHemisphere, 2.0), 0.2, 1.); // Blue light is less scattered than red light
cloudsColor.a *= clamp(mixAmountHemisphere, 0.1, 1.);

For calculating the shadow of cloud, we first compute dot(vNormal, sunDir) * vNormal - sunDir in texture coordinates. This means a vector parallel to the surface, indicating the position of the cloud texture UV map through which sunlight penetrates from the surface texture UV position.

Then, inverse(vTbn) converts the vector from world to local coordinate system, and a magic number 0.005 implies the height of clouds. Next, cloudsShadow is the cloud texture value that affects the surface. Therefore, mixAmountTexture decreases depending on the thickness of cloud that sunlight penetrates.

vec3 translVec = 0.0005 * inverse(vTbn) * (dot(vNormal, sunDir) * vNormal - sunDir);
vec4 cloudsShadow = texture2D(u_cloudTexture, vUv - translVec.xy);
mixAmountTexture *= (1. - 0.5*cloudsShadow.a);

Eclipse

In the previous article, we render the eclipse shadow of Moon on the surface of Earth. However, since the shadow map of Three.js is based on spherical coordinates, its resolution decreases when the distance from the light source increases. Also, there are few objects in a broad universe space, it is inefficient to use the shadow map of Three.js. Thus, we’re going to render a shadow on the object directly using shaders. By using this method, the quality of shadow does not deteriorate even as the distance from the light source increases.

The below figure describes how to compute the brightness of the surface while considering eclipse.

In the above, I have computed how much lighting source is hided by the Moon. Let \(R_s\) and \(R_m\) be the actual radius of the Sun and Moon. Given vectors, \({v}_{se}\) and \({v}_{me}\), denoting a vector from Earth to the Sun and Moon, respectively, the apparent radius of the Sun and Moon, \(\theta_{s}\) and \(\theta_{m}\), are

\[\theta_{s} = \sin^{-1} {\frac{R_s}{d_{se}}} \approx \frac{R_s}{d_{se}}\]

and

\[\theta_{m} = \sin^{-1} {\frac{R_m}{d_{me}}} \approx \frac{R_m}{d_{me}},\]

where \(d_{se}=\|{v}_{se}\|\) and \(d_{me}=\|{v}_{me}\|\). Also, the apparent distance between the Sun and Moon is \(\theta_{sm} = \frac{ {v}_{se} \cdot {v}_{me}}{ \|{v}_{se}\| * \|{v}_{me}\|}\). Then, the brightness of the surface depends on the relationship between \(\theta_s, \theta_m\), and \(\theta_{sm}\). When \(\theta_{sm}\) is larger than \(\theta_s + \theta_m\), eclipse does not appear. In this case, the apparent surface of the lighting source is \(\pi R_s^2\), and it is applied to the shader as 1 by scaling with \(\pi R_s^2\). Let us call this scaled value as a brightness factor.

If \(\theta_m < \theta_s\) and \(0 < \theta_{sm} < \theta_s - \theta_m\) is satisfied, the Moon is completely inside the sun, so the brightness factor is

\[\frac{\pi R_s^2 - \pi (d_{se}\theta_m)^2}{\pi R_s^2} = 1 - \frac{\theta^2_m}{\theta^2_s},\]

where \(d_{se}\theta_m\) is referred to as the apparent radius assuming that the Moon and the sun are at the same distance from the Earth, not actual radius of the Moon.

Moreover, if \(\theta_s < \theta_m\) and \(0 < \theta_{sm} < \theta_m - \theta_s\) are satisfied, the brightness factor is zero, since the sun is fully hided by the Moon. Otherwise, When \(abs(\theta_s - \theta_m) < \theta_{sm} < \theta_s + \theta_m\) is satisfied, the sun is partially hided. In this case, the brightness factor is

\[\frac{1}{\pi \theta^2_s} (\pi \theta^2_s - \phi_s \theta^2_s + \frac{1}{2}\theta^2_s\sin(2\phi_s) -\phi_m \theta^2_m + \frac{1}{2}\theta^2_m \sin(2 \phi_m)),\]

where

\[\begin{align} \phi_s &= \cos^{-1}(x/\theta_s), \nonumber\\ \phi_m &= \cos^{-1}((\theta_{sm}-x)/\theta_m), \end{align}\]

and

\[x = \frac{1}{2\theta_{sm}} * (\theta_{sm}^2 + \theta_s^2 - \theta_m^2)\]

that is the intersected position between

\[x^2 + y^2 = \theta_s^2\]

and

\[(x-\theta_{sm})^2 + y^2 = \theta_m^2.\]

The above equations are summarized in the below function.

#define PI (3.141592)

float eclipse(float angleBtw, float angleLight, float angleObs) {
    float angleRatio2 = pow(angleObs / angleLight, 2.);
    float value;
    if (angleBtw > angleLight - angleObs && angleBtw < angleLight + angleObs) {
        if (angleBtw < angleObs - angleLight) {
            value = 0.;
        }else {
            float x = 0.5/angleBtw * (angleBtw*angleBtw + angleLight*angleLight - angleObs*angleObs);
            float ths = acos(x/angleLight);
            float thm = acos((angleBtw-x)/angleObs);
            value = 1./PI * (PI - ths + 0.5 * sin(2. * ths) - thm * angleRatio2 + 0.5 * angleRatio2 * sin(2. * thm));
        }
    } else if (angleBtw > angleLight + angleObs)
        value = 1.;
    else { // angleBtw < angleLight - angleObs
        value = 1. - angleRatio2;
    }

    return clamp(value, 0., 1.);
}

Then, the brightness of the surface is multiplied by the brightness factors for all moon.

// Eclipse
vec3 surfacePosition = u_position + vPosition;
float distSurfaceToSun = length(u_sunRelPosition);
float cosAngleBtwSunMoon = dot(sunDir, normalize(u_moonPosition - surfacePosition));
float angleBtwSunMoon = acos(cosAngleBtwSunMoon);
float distSurfaceToMoon = length(u_moonPosition - surfacePosition);

mixAmountHemisphere *= eclipse(angleBtwSunMoon, asin(u_sunRadius/distSurfaceToSun), asin(u_moonRadius/distSurfaceToMoon));

Please insert the below line into the normal map processing to apply eclipse shadow to the ground.

mixAmountTexture *= mixAmountHemisphere;

Atmosphere

Thankfully, the Earth has an atmosphere. To generate our blue atmosphere, we have to consider that the atmosphere is visible remarkably under the light. Thus, we compute the cosine angle between sun and normal vector again. Then, in common with the glow effect in [[Create realistic Sun]], I’ve created an ambient circle with u_color, and masked with mixAmount. Notice that vNormalView denotes a normal vector of the object, represented in the camera coordinates.

uniform vec3 u_sunRelPosition;
uniform vec3 u_color; // the color of atmosphere

varying vec3 vNormal;
varying vec3 vNormalView;
varying vec3 vPosition;

void main( void ) {
    vec3 sunDir = u_sunRelPosition;
    vec3 sunDirUnit = normalize(sunDir);

    // Day and night texture
    float cosAngleSunToNormal = dot(vNormal, sunDirUnit); // Compute cosine sun to normal
    float mixAmount = 1. / (1. + exp(-7. * (cosAngleSunToNormal + 0.1))); // Sharpen the edge beween the transition

    // Atmosphere
    float raw_intensity = 3. * max(dot(vPosition, vNormalView), 0.);
    float intensity = pow(raw_intensity, 3.);
    
    gl_FragColor = vec4(u_color, intensity) * mixAmount;
}

The result is as follows. By the way, I’ve changed the background color to black to make it easier to recognize the atmosphere.

Fresnel

Finally, this is the last step. The Fresnel effect deals with the intensity of reflection depending on the angle of incidence. As the angle gets small, the reflection becomes strong. Thus, the farther from the center of the Earth, i.e. the greater the angle between the camera-surface vector and the normal vector of the surface, the less transparent the atmosphere should be. As the same with the above atmosphere, Fresnel values are also masked with day-and-night amount, mixAmount.

uniform vec3 u_sunRelPosition;
uniform vec3 u_color;

varying vec3 vNormal;
varying vec3 vNormalView;
varying vec3 vPosition;

void main() {
    vec3 sunDir = u_sunRelPosition;
    vec3 sunDirUnit = normalize(sunDir);

    // Day and night texture with eclipse
    float cosAngleSunToNormal = dot(vNormal, sunDirUnit); // Compute cosine sun to normal
    float mixAmount = 1. / (1. + exp(-7. * (cosAngleSunToNormal + 0.1))); // Sharpen the edge beween the transition

    float fresnelTerm = (1. + dot(normalize(vPosition), normalize(vNormalView)));
    fresnelTerm = pow(fresnelTerm, 2.0);
    
    gl_FragColor = vec4( u_color, 1. ) * fresnelTerm * mixAmount;
}

As compared the below image to the previous one, you can see the surface blurring at the edge of the Earth.

Entire code

Up to here, the overall fragment and vertex shader codes, and Three.js javascript code are below.

Earth’s fragment shader

uniform sampler2D u_dayTexture;
uniform sampler2D u_nightTexture;
uniform sampler2D u_normalTexture;
uniform sampler2D u_specTexture;
uniform sampler2D u_cloudTexture;
uniform vec3 u_sunRelPosition; // the relative position of light source
uniform float u_normalPower;
uniform vec3 u_position;
uniform vec3 u_moonPosition;
uniform float u_moonRadius;
uniform float u_sunRadius;

varying mat3 vTbn;
varying vec2 vUv; // texture UV map
varying vec3 vNormal; // normal vector at surface
varying vec3 vPosition;

#define PI (3.141592)

float eclipse(float angleBtw, float angleLight, float angleObs) {
    float angleRatio2 = pow(angleObs / angleLight, 2.);
    float value;
    if (angleBtw > angleLight - angleObs && angleBtw < angleLight + angleObs) {
        if (angleBtw < angleObs - angleLight) {
            value = 0.;
        }else {
            float x = 0.5/angleBtw * (angleBtw*angleBtw + angleLight*angleLight - angleObs*angleObs);
            float ths = acos(x/angleLight);
            float thm = acos((angleBtw-x)/angleObs);
            value = 1./PI * (PI - ths + 0.5 * sin(2. * ths) - thm * angleRatio2 + 0.5 * angleRatio2 * sin(2. * thm));
        }
    } else if (angleBtw > angleLight + angleObs)
        value = 1.;
    else { // angleBtw < angleLight - angleObs
        value = 1. - angleRatio2;
    }

    return clamp(value, 0., 1.);
}

void main( void ) {
    vec3 sunDir = normalize(u_sunRelPosition);

    // Day and night texture with eclipse
    vec3 dayColor = texture2D( u_dayTexture, vUv ).rgb;
    vec3 nightColor = texture2D( u_nightTexture, vUv ).rgb;

    float cosAngleSunToNormal = dot(vNormal, sunDir); // Compute cosine sun to normal
    float mixAmountTexture = 1. / (1. + exp(-20. * cosAngleSunToNormal));
    float mixAmountHemisphere = mixAmountTexture;
    
    // 2. Eclipse
    vec3 surfacePosition = u_position + vPosition;
    float distSurfaceToSun = length(u_sunRelPosition);
    float cosAngleBtwSunMoon = dot(sunDir, normalize(u_moonPosition - surfacePosition));
    float angleBtwSunMoon = acos(cosAngleBtwSunMoon);
    float distSurfaceToMoon = length(u_moonPosition - surfacePosition);

    mixAmountHemisphere *= eclipse(angleBtwSunMoon, asin(u_sunRadius/distSurfaceToSun), asin(u_moonRadius/distSurfaceToMoon));

    // Normal map texture
    vec3 t_normal = texture2D( u_normalTexture, vUv ).xyz * 2.0 - 1.0;
    vec3 normal = normalize(vTbn * t_normal);
    float cosAngleSunToSurface = dot(normal, sunDir); // Compute cosine sun to normal
    mixAmountTexture *= 1.0 + u_normalPower * (cosAngleSunToSurface - cosAngleSunToNormal);
    mixAmountTexture *= mixAmountHemisphere;
    mixAmountTexture = clamp(mixAmountTexture, 0., 1.);

    // Cloud shadow
    vec3 translVec = 0.0005 * inverse(vTbn) * (vNormal - sunDir);
    vec4 cloudsShadow = texture2D(u_cloudTexture, vUv - translVec.xy);
    mixAmountTexture *= (1. - 0.5*cloudsShadow.a);

    // Combine night and day colors
    vec3 color = mix( nightColor, dayColor, mixAmountTexture ); // Select day or night texture

    // Specular map texture with reflection
    float reflectRatio = texture2D(u_specTexture, vUv).r;
    reflectRatio = 0.3 * reflectRatio + 0.1;
    vec3 reflectVec = reflect(-sunDir, normal); // reflected vector of sunlight
    float specPower = clamp(dot(reflectVec, normalize(cameraPosition - surfacePosition)), 0., 1.); // dot product between reflected light and camera vector
    color += mixAmountTexture * pow(specPower, 2.0) * reflectRatio;

    // cloud
    vec4 cloudsColor = texture2D(u_cloudTexture, vUv);
    cloudsColor.r *= clamp(mixAmountHemisphere, 0.2, 1.);
    cloudsColor.g *= clamp(pow(mixAmountHemisphere, 1.5), 0.2, 1.);
    cloudsColor.b *= clamp(pow(mixAmountHemisphere, 2.0), 0.2, 1.); // Blue light is less scattered than red light
    cloudsColor.a *= clamp(mixAmountHemisphere, 0.1, 1.);
    color = color * (1.0 - cloudsColor.a) + cloudsColor.rgb * cloudsColor.a;

    // render
    gl_FragColor = vec4(color, 1.);
}

Earth’s vertex shader

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying mat3 vTbn;

attribute vec4 tangent; // "geometry.computeTangents()" is needed.

void main() {
    vUv = uv;
    vNormal = normalize(mat3(modelMatrix) * normal);
    vPosition = mat3(modelMatrix) * position;
    
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

    vec3 t = normalize(tangent.xyz);
    vec3 n = normalize(normal.xyz);
    vec3 b = normalize(cross(t, n));

    t = mat3(modelMatrix) * t;
    b = mat3(modelMatrix) * b;
    n = mat3(modelMatrix) * n;
    vTbn = mat3(t, b, n);
}

Add-on vertex shader

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);
}

main.js

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import vertex from PATH_TO_EARTH_VERTEX;
import fragment from PATH_TO_EARTH_FRAGMENT;
import addon_vertex from PATH_TO_ADDON_VERTEX;
import atmosphere from PATH_TO_ATMOSPHERE_FRAGMENT;
import fresnel from PATH_TO_FRESNEL_FRAGMENT;

const canvas = document.createElement("canvas");
document.body.appendChild(canvas);

const renderer = new THREE.WebGLRenderer({canvas: canvas, alpha: true, antialias: true});
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 5);

const geometry_sphere = new THREE.SphereGeometry(1, 30, 30);
geometry_sphere.computeTangents();

const material_sun = new THREE.MeshBasicMaterial({color: 0xffaa00, opacity: 0, transparent: true});
const sun = new THREE.Mesh(geometry_sphere, material_sun);

const material_earth = new THREE.ShaderMaterial({
    uniforms: {
        u_dayTexture: { value: new THREE.TextureLoader().load( './assets/2k_earth_daymap.jpg') },
        u_nightTexture: { value: new THREE.TextureLoader().load( './assets/earthlights2k.jpg') },
        u_normalTexture: { value: new THREE.TextureLoader().load( './assets/earthnormal2k.jpg') },
        u_specTexture: { value: new THREE.TextureLoader().load( './assets/2k_earth_specular_map.tif') },
        u_cloudTexture: { value: new THREE.TextureLoader().load( './assets/earthcloud.png')},
        u_normalPower: { value: 5.0 },
        u_sunRelPosition: { value: new THREE.Vector3(0,0,0)},
        u_position: { value: new THREE.Vector3(0,0,0)},
        u_moonPosition: { value: new THREE.Vector3(0,0,0)},
        u_moonRadius: { value: 0.05},
        u_sunRadius: { value: 0.2},
    },
    vertexShader: vertex,
    fragmentShader: fragment,
})
const earth = new THREE.Mesh(geometry_sphere, material_earth);
earth.scale.set(0.2, 0.2, 0.2);

const material_earth_atmosphere = new THREE.ShaderMaterial({
    uniforms: {
        u_sunRelPosition: { value: new THREE.Vector3(0,0,0)},
        u_color: { value: new THREE.Vector3(.45,.55,1)},
    },
    vertexShader: addon_vertex,
    fragmentShader: atmosphere,
    transparent: true,
    side: THREE.BackSide,
    depthTest: true,
    depthWrite: false,
});

const earth_atmosphere = new THREE.Mesh(geometry_sphere, material_earth_atmosphere);
earth_atmosphere.scale.set(1.05, 1.05, 1.05);
earth.add(earth_atmosphere);

const material_earth_fresnel = new THREE.ShaderMaterial({
    uniforms: {
        u_sunRelPosition: { value: new THREE.Vector3(0,0,0)},
        u_color: { value: new THREE.Vector3(.45,.55,1)},
    },
    vertexShader: addon_vertex,
    fragmentShader: fresnel,
    transparent: true
});

const earth_fresnel = new THREE.Mesh(geometry_sphere, material_earth_fresnel);
earth_fresnel.scale.set(1.0001, 1.0001, 1.0001);;
earth.add(earth_fresnel);

const material_moon = new THREE.MeshLambertMaterial();
const moon = new THREE.Mesh(geometry_sphere, material_moon);
moon.scale.set(0.05, 0.05, 0.05);

const loader = new THREE.TextureLoader();
loader.load('./assets/moonmap2k.jpg', (texture)=>{
    material_moon.map = texture;
    material_moon.needsUpdate = true;
});

const light = new THREE.PointLight(0xffffff, 15);
light.position.set(0, 0, 0);
light.castShadow = true;
// light.shadow.mapSize = new THREE.Vector2(4096, 4096);
// light.shadow.radius = 20;

const earth_orbit = new THREE.Object3D();
const earth_equator = new THREE.Object3D();
const moon_orbit = new THREE.Object3D();
// earth_equator.rotateZ(23.5*Math.PI/180);

const scene = new THREE.Scene();
scene.add(sun);
sun.add(light);
scene.add(earth_orbit);
earth_orbit.add(earth_equator);
earth_equator.add(earth);
earth_equator.add(moon_orbit);
moon_orbit.add(moon);

const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

camera.position.set(0,0,0.6)

const w_moon = 0.5;
const w_orbit = 0;
const w_rotate = 0.0;

function updateSystem(sec) {
    moon.position.set(0.4*Math.cos(w_moon*sec), 0, -0.4*Math.sin(w_moon*sec));
    sun.position.set(3*Math.cos(w_orbit*sec), 0, -3*Math.sin(w_orbit*sec));
    material_earth.uniforms.u_sunRelPosition.value.x = 3*Math.cos(w_orbit*sec);
    material_earth.uniforms.u_sunRelPosition.value.y = 0;
    material_earth.uniforms.u_sunRelPosition.value.z = -3*Math.sin(w_orbit*sec);
    material_earth.uniforms.u_moonPosition.value.x = 0.4*Math.cos(w_moon*sec);
    material_earth.uniforms.u_moonPosition.value.y = 0;
    material_earth.uniforms.u_moonPosition.value.z = -0.4*Math.sin(w_moon*sec);
    material_earth_atmosphere.uniforms.u_sunRelPosition.value.x = 3*Math.cos(w_orbit*sec);
    material_earth_atmosphere.uniforms.u_sunRelPosition.value.y = 0;
    material_earth_atmosphere.uniforms.u_sunRelPosition.value.z = -3*Math.sin(w_orbit*sec);
    material_earth_fresnel.uniforms.u_sunRelPosition.value.x = 3*Math.cos(w_orbit*sec);
    material_earth_fresnel.uniforms.u_sunRelPosition.value.y = 0;
    material_earth_fresnel.uniforms.u_sunRelPosition.value.z = -3*Math.sin(w_orbit*sec);
    material_sun.uniforms.u_time.value = Date.now()/1000 - time_init;
    // earth.rotateY(w_rotate);
    sun.rotateY(w_rotate);
}

function animate (msec) {
    requestAnimationFrame(animate);
    
    updateSystem(msec * 0.001);

    controls.update();
    renderer.render(scene, camera);
}
animate();