three.js implements sunshine analysis and simulates changes in the sun’s altitude angle and azimuth angle based on local (dimension and time)

1. Analysis

Recently, there was a need to do a solar illumination simulation for a certain building, and the sunlight changes from 8 o’clock to 17 o’clock. To better simulate the changes of the sun throughout the day, we only need to know the solar altitude angle and solar azimuth angle of the current observation point. In order to be more realistic, we need more sunlight colors. Perform difference calculations;

We all know that the earth’s rotation around the sun is one of the basic phenomena of earth kinematics and is called revolution. This motion is a periodic variation in the Earth’s elliptical orbit around the Sun, which determines the length of the year.

When talking about the earth’s revolution around the sun, we need to take into account the changes in thesun’s direct point. The direct solar point is the place on the earth where the sun’s rays shine vertically. This point changes periodically as the earth moves around the sun.

Over the course of a year, the sun’s direct point moves back and forth across the ecliptic plane, creating two vernal equinoxes and two autumnal equinoxes. Specifically:

  1. Vernal Equinox Time: When the direct point of the sun is above the equator, the northern hemisphere of the earth welcomes spring and the southern hemisphere welcomes autumn. This moment marks the equal length of day and night and the official start of spring.

  2. Autumn Equinox Time: When the sun’s direct point returns to the equator again, the earth’s northern hemisphere welcomes autumn and the southern hemisphere welcomes spring. Likewise, the lengths of day and night are equal again, and autumn officially begins.

This change in the sun’s direct point causes the alternation of different seasons in various parts of the earth. For example, when the sun’s direct point is in the northern hemisphere, the northern hemisphere will experience summer and the southern hemisphere will experience winter, and vice versa.

Understand the movement pattern of the direct sun point throughout the year. That’s easy. We only need to know the dimensions of the target point to be observed.

The observation point we chose this time is Changsha. Why? Because the project happens to be in Changsha, hahaha

Longitude and latitude of Changsha=28 °11′49″N,112 °58′42″

Then we went to the Internet to check (actually we used chatGTP to check^^) the changes in the solar altitude and azimuth angle of the Changsha almanac in the past five years, and then took the average value

The International Astronomical Union can be found. After sorting, we got the following data

// Changes in solar azimuth angle and solar altitude angle from 8 a.m. to 5 p.m. in Changsha at the spring equinox, summer solstice, autumnal equinox, and winter solstice. Data source: International Astronomical Union www.timeanddate.com
const CHANGSHA = [
  [
    [22.86, 30.45, 38.11, 44.94, 49.03, 44.68, 37.68, 30.11, 22.62, 15.71],
    [
      119.62, 126.43, 143.12, 165.6, 180, 195.63, 218.37, 235.08, 246.64,
      253.77,
    ],
  ],

  [
    [61.19, 68.25, 74.15, 77.59, 77.86, 74.77, 68.19, 60.09, 51.47, 43.13],
    [
      84.51, 98.94, 125.33, 156.77, 178.06, 198.51, 225.16, 239.45, 250.1,
      256.54,
    ],
  ],

  [
    [22.89, 30.16, 37.51, 44.26, 48.31, 43.99, 36.85, 29.13, 21.49, 14.38],
    [
      120.38, 127.25, 144.28, 167.62, 180.0, 192.64, 214.02, 230.44, 241.84,
      249.24,
    ],
  ],

  [
    [22.04, 29.08, 35.36, 40.43, 43.03, 40.76, 34.97, 28.38, 21.43, 15.06],
    [120.5, 127.8, 144.57, 167.15, 180, 193.71, 216.14, 232.91, 244.67, 252.4],
  ],
];

export { CHANGSHA };

With these data, how to generate our sun in the sky based on the solar altitude angle and solar azimuth angle? Is it possible to write it myself using shaders? Absolutely not. It’s not impossible to play this way, you have to pay more.

We can use the Sky class provided by three.js to generate the sky, which exposes several uniform values to adjust the parameters of the sun, the most important of which are our phi and theta, and then convert the spherical coordinates into three-dimensional coordinates Assign value to sunPosition

import * as THREE from "three";
import { Sky } from "three/examples/jsm/objects/Sky.js";

export default class Sun {
  sky: Sky;
  constructor(
    scene: THREE.Scene,
    renderer: THREE.WebGLRenderer
  ) {
    const sun = new THREE.Vector3();

    this.sky = new Sky();
    this.sky.scale.setScalar(40000);
    scene.add(this.sky);

    // initial value
    const effectController = {
      turbidity: 20,
      rayleigh: 3,
      mieCoefficient: 0.005,
      mieDirectionalG: 0.7,
      elevation: 2,
      azimuth: 180,
    };

    const uniforms = this.sky.material.uniforms;
    uniforms["turbidity"].value = effectController.turbidity;
    uniforms["rayleigh"].value = effectController.rayleigh;
    uniforms["mieCoefficient"].value = effectController.mieCoefficient;
    uniforms["mieDirectionalG"].value = effectController.mieDirectionalG;

    const phi = THREE.MathUtils.degToRad(90 - effectController.elevation);
    const theta = THREE.MathUtils.degToRad(effectController.azimuth);

    sun.setFromSphericalCoords(1, phi, theta);

    uniforms["sunPosition"].value.copy(sun);

    renderer.toneMappingExposure = 1;
  }
}

Now that the sun is in the sky, we can base it on the position of the sun and our position. Our position can be at the origin by default. We can calculate the direction between the two points, and then update the direction or position of the parallel light based on the direction. Yes, because Sky in three.js draws the sun, but this sun is not a light source, it is just a self-illumination.

All the raw materials are ready, let’s get started

Prepare our direct light to simulate sunlight + hemispheric light (the back side can also be illuminated), and the sun’s halo

import {
  DirectionalLight,
  HemisphereLight,
  Color,
  PointLight,
  TextureLoader,
} from "three";
import {
  Lensflare,
  LensflareElement,
} from "three/examples/jsm/objects/Lensflare.js";

function createDirLight(dirColor: string | number | Color) {
  const dirLight = new DirectionalLight(dirColor);
  //Set direct light shadow
  dirLight.castShadow = true;
  dirLight.shadow.mapSize.width = Math.pow(2, 13);
  dirLight.shadow.mapSize.height = Math.pow(2, 13);

  const d = 400;
  // shadow size
  dirLight.shadow.camera.left = -d;
  dirLight.shadow.camera.right = d;
  dirLight.shadow.camera.top = d;
  dirLight.shadow.camera.bottom = -d;

  dirLight.shadow.camera.far = 800;
  dirLight.shadow.camera.near = 10;

  return dirLight;
}

function hlight() {
  const hemiLight = new HemisphereLight(0xb1e1ff, "#1e1e1e", 0.4);
  hemiLight.color.setHSL(0.6, 1, 0.6);
  hemiLight.groundColor.setHSL(0.095, 1, 0.75);
  hemiLight.position.set(0, 50, 0);
  return hemiLight;
}

function addLight(light: PointLight) {
  const lensflare = new Lensflare();
  const textureLoader = new TextureLoader();
  const textureFlare0 = textureLoader.load("textures/lensflare/lensflare0.png");
  const textureFlare3 = textureLoader.load("textures/lensflare/lensflare3.png");
  lensflare.addElement(
    new LensflareElement(textureFlare0, 250, 0, light.color)
  );
  lensflare.addElement(new LensflareElement(textureFlare3, 60, 0.6));
  lensflare.addElement(new LensflareElement(textureFlare3, 70, 0.7));
  lensflare.addElement(new LensflareElement(textureFlare3, 120, 0.9));
  lensflare.addElement(new LensflareElement(textureFlare3, 70, 1));
  light.add(lensflare);
}

export { createDirLight, hlight, addLight };

Also, the data we got is from 8 am to 17 pm, but when we drag the mouse to slide the time, the changed value range is 8-17. Just pass the above data, and then use some logical influence Just go to the sun altitude angle, sun azimuth angle, sun color value, sun halo position, etc.;

In fact, it is some simple data conversion, such as difference, angle to radian, spherical coordinates and Cartesian coordinates. These can be seen in the code, and can also be seen from the method name. For example, degToRad is angle to radian setFromSphericalCoords

To convert spherical coordinates to Cartesian coordinates, multiplyScalar is the scaling vector (generally, you know the direction, and then zoom down to get a new position). It feels a bit verbose to write, hahaha

But the sliding step size is 0.1, so what if there will be decimal places? The data we just gave are all on the whole point; we can find the time with decimal places through the difference.
three.js provides us with many difference tools, such as the one-dimensional difference MathUtils.lerp, which we can use for time or angle, and the color difference lerpColors, < /strong>These two are mainly used. Let’s look at the code directly below.

<template>
    <div class="control">
        <div class="top">
            <div>{<!-- -->{ }}</div>
            <el-button type="primary" @click="showDrawer">
                Building selection
            </el-button>
        </div>
        <div class="slider">
            <span>{<!-- -->{ min + ':00' }}</span>
            <el-slider class="bar" v-model="time" :step="0.1" :min="min" :max="max" @input="updateSunPostion" />
            <span>{<!-- -->{ max + ':00' }}</span>
            <el-button type="primary" :icon="playIcon" round @click="clickHandler" />
        </div>
        <div class="bnts">
            <el-button type="primary" plain :key="season" v-for="(season, index) in seasons" @click="getSeason(index)">
                {<!-- -->{ season }}
            </el-button>
        </div>
    </div>
    <el-drawer title="Select floor" v-model="drawer" direction="btt" destroy-on-close width="50%" :append-to-body="true" size="34%"
        @close="closeDrawerHandler">
        <floorSelectTor @checked="getFloor"></floorSelectTor>
    </el-drawer>
</template>

<script setup lang="ts">
import { ref, nextTick, computed, toRaw } from 'vue'
import { ElSlider, ElButton, ElDrawer } from 'element-plus';
import Sun from './index'
import useStore from '../store';
import { Vector3, MathUtils, WebGLRenderer, Scene, Color, PointLight, PerspectiveCamera, DirectionalLight } from 'three'
import { VideoPlay, VideoPause } from '@element-plus/icons-vue'
import { CHANGSHA } from './data'
import { addLight, createDirLight } from './lights'
import floorSelectTor from './floorSelector.vue'
import TWEEN from '@tweenjs/tween.js'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

const store = useStore()

const drawer = ref(false)
const showDrawer = () => {
    drawer.value = true
    store.buildingSet.forEach((item: any) => {
        item.material.opacity = 1;
    })
}
let targetFloor = 0
const getFloor = (floor: number) => {
    targetFloor = floor
}

const closeDrawerHandler = () => {
    const targetBuilding = scene.getObjectByName('text' + targetFloor + '#')
    if (targetBuilding) {
        new TWEEN.Tween(control.target)
            .to(targetBuilding.position)
            .easing(TWEEN.Easing.Quadratic.InOut)
            .start();
        store.moveViwer(targetFloor + '#')
    }
}


let renderer: WebGLRenderer
let scene: Scene
let camera: PerspectiveCamera
let control: OrbitControls

let sun: sun

const playState = ref(false)
const clickHandler = () => {
    if (playState.value) {
        cancelAnimationFrame(frame)
    } else {
        paly()
    }
    playState.value = !playState.value
}
const playIcon = computed(() => {
    return playState.value ? VideoPause : VideoPlay
})

let frame: any
const time = ref(8)
const [min, max] = [8, 17] // time

const paly = () => {
    time.value + = 0.1
    updateSunPostion()
    if (time.value >= max) {
        time.value = 8
    }

    frame = requestAnimationFrame(paly)

}

const dirColor = new Color('#fdd885')
const morningColor = new Color('#AE8110')
const noonColor = new Color('#f9db56')

// backlight
const backlight = new DirectionalLight('#89927d', 0.4)

// halo effect
const lenPosiotn = new Vector3()
const Plight = new PointLight(dirColor, 0.2, 1500, 0);


const sunDir = new Vector3() // Sunlight position
const hlightPosition = new Vector3() // Backlight position


const seasons = ['Spring Equinox', 'Summer Solstice', 'Autumnal Equinox', 'Winter Solstice']
let SphericalCoords = CHANSHA[0]
function getSeason(index: number) {
    SphericalCoords = CHANSHA[index]
    updateSunPostion()

}

function updateSunPostion() {
    const index = time.value - 8
    const curent = Math.floor(index)
    const next = Math.ceil(index)

    // Solar elevation angle
    const phi = MathUtils.degToRad(90 - MathUtils.lerp(SphericalCoords[0][curent], SphericalCoords[0][next], index - curent))
    // Sun azimuth angle
    const theta = MathUtils.degToRad(180 - MathUtils.lerp(SphericalCoords[1][curent], SphericalCoords[1][next], index - curent))

    const ratio = (time.value - min) / (max - min)

    const turbidity = ratio * 20
    const rayleigh = Math.abs(ratio - 0.5) * 4

    sunDir.setFromSphericalCoords(1, phi, theta);
    lenPosiotn.setFromSphericalCoords(800, phi, theta)
    Plight.position.copy(lenPosiotn)

    // backlight position
    hlightPosition.setFromSphericalCoords(800, Math.PI / 2 - phi, Math.PI - theta)
    backlight.position.copy(hlightPosition)

    // sun color
    const colorRatio = Math.abs(ratio - 0.5) * 2
    Plight.color.set(dirColor.lerpColors(noonColor, morningColor, colorRatio))

    // direct light
    dirLight.color.set(dirColor.lerpColors(noonColor, morningColor, colorRatio))
    dirLight.position.copy(sunDir.multiplyScalar(350))

    const uniforms = sun.sky.material.uniforms;
    uniforms["turbidity"].value = turbidity;
    uniforms["rayleigh"].value = rayleigh;
    uniforms["mieCoefficient"].value = 0.005;
    uniforms["mieDirectionalG"].value = ratio;

    uniforms["sunPosition"].value.copy(sunDir);

    renderer.render(scene, camera)
}

const dirLight = createDirLight(new Color(0.08, 0.8, 0.5))
nextTick(() => {
    scene = toRaw(store.scene)
    renderer = toRaw(store.renderer)
    camera = toRaw(store.camera)
    control = toRaw(store.control)

    sun = new Sun(scene, renderer)
    scene.add(dirLight);

    addLight(Plight);
    scene.add(Plight)
    scene.add(backlight)

    updateSunPostion()
})
</script>

<style scoped>
.control {
    height: 200px;
    width: min(100%, 500px);
    background-color: rgba(203, 217, 110, 0.4);
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    border-radius: 10px;
    overflow: hidden;
    z-index: 2;
}

.slider {
    width: 90%;
    display: flex;
    justify-content: space-between;
    line-height: 35px;
    margin: 0 20px;
}

.bar {
    width: 70%;
}

.bnts {
    display: flex;
    justify-content: space-around;
    align-items: center;
    width: 100%;
    margin-top: 10px;
}

.top {
    width: 90%;
    display: flex;
    justify-content: space-around;
    align-items: center;
    font-size: 22px;
    color: black;
    font-weight: 600;
    margin: 10px 20px;
}
</style>

The scene initialization or loop playback inside is not the core, so I won’t go into too much detail. Let’s see the final effect.

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Vue entry skill treevue3 basics (JS)Vue3 current situation 39469 people are learning the system

syntaxbug.com © 2021 All Rights Reserved.