three.js实现日照分析,根据当地(维度和时间)模拟太阳高度角和方位角的变化

一、分析

最近有个需求是要给某楼盘做一个太阳光照模拟,从8点到17点的日照变化。要更好的模拟太阳在一天中的变化,我们只需要知道当前观测点的太阳高度角太阳方位角,为了更加真是还要多阳光颜色进行差值计算;

我们都知道,地球绕太阳转是地球运动学的基本现象之一,被称为公转。这一运动是地球围绕太阳的椭圆轨道上的周期性变化,它决定了一年的长度。

当谈及地球绕太阳公转时,我们需要考虑到太阳直射点的变化规律。太阳直射点是地球上太阳光线垂直照射的地方,这个点会随着地球绕太阳运动而发生周期性的变化。

在一年的时间里,太阳直射点会在黄道平面上来回移动,形成了两次春分和两次秋分。具体来说:

  1. 春分时刻: 当太阳直射点位于赤道上方时,地球的北半球迎来春天,南半球迎来秋天。这一时刻标志着昼夜长度相等,春天正式开始。

  2. 秋分时刻: 当太阳直射点再次回到赤道上方时,地球的北半球迎来秋天,南半球迎来春天。同样,昼夜长度再次相等,秋天正式开始。

太阳直射点的这种变化规律导致了地球各地不同季节的交替。例如,当太阳直射点位于北半球时,北半球将经历夏季,而南半球则经历冬季,反之亦然

知道了,一年中太阳直射点的运动规律。那就好办了,我们只需要知道要观测的目标点的维度就可以了。

这次我们选择的观测点是长沙,为什么呢,因为刚好项目就是长沙的,哈哈哈

 长沙经纬度=28 °11′49〃N,112 °58′42〃

然后我们去网上查询了(其实是用chatGTP查询的啦^^)近五年长沙年历太阳高度角和方位角的变化,然后取了平均值

国际天文学联合会,可以查到,经过整理我们得到了以下数据

// 长沙春分,夏至,秋分,冬至从早上8点到下午5点太阳方位角和太阳高度角变化,数据来源国际天文学联合会 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 };

有了这些数据了,那如何根据太阳高度角和太阳方位角,在天空中生成我们的太阳呢?难不成自己用着色器来写?万万不可啊。这么玩的话也不是不行,得加钱啊。

我们可以用three.js给我们提供的Sky类来生成天空,暴露出了几个uniforms的值用来调整太阳的参数,其中最重要的就是我们的phi, theta,然后将球坐标转成三维坐标赋值给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);

    // 初始值
    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;
  }
}

天空的太阳有了,那我们可以根据太阳的位置和我们的位置,我们的位置默认在原点就好了,两个点之间可以算出方向,然后根据方向,更新平行光的方向或者位置就可以了,因为three.js中的Sky是绘制出了太阳,但是这个太阳不是光源啊,只是一个自发光。

所有原材料都准备了,那就开干吧

准备一下我们的直射光用来模拟阳光+半球光(可以背面也照亮)、还有太阳的光晕

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);
  // 设置直射光阴影
  dirLight.castShadow = true;
  dirLight.shadow.mapSize.width = Math.pow(2, 13);
  dirLight.shadow.mapSize.height = Math.pow(2, 13);

  const d = 400;
  // 阴影大小
  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 };

还有我们得到的数据是早上8点到下午17点的,但是当我们拖动鼠标滑动时间的时候,变化的值是的范围是8-17只需通过上面的数据,然后再用一些逻辑影响到太阳高度角、太阳方位角、太阳颜色值、太阳光晕的位置等就可以了;

其实就是一些简单的数据转换,例如差值、角度转弧度、球坐标和笛卡尔坐标的互转,这些都可以在代码里看到,从方法名也可以看出来,例如degToRad就是角度转弧度setFromSphericalCoords

球坐标转笛卡尔坐标,multiplyScalar就是缩放向量(一般是知道方向了,然后朝着该放下缩放,得到一个新的位置),感觉写有点啰嗦了,哈哈哈

但是滑动的步长是0.1,那就会出现小数位那怎么呢,我们刚刚的数据可都是整点啊;我们可以通过差值的方式求出这个带有小数位的时间。
three.js给我们提供的很多差值工具例如用于一维的差值MathUtils.lerp,我们时间或者角度都可以用这个,还有颜色差值lerpColors,主要用到这两个,下面直接看代码

<template>
    <div class="control">
        <div class="top">
            <div>{{  }}</div>
            <el-button type="primary" @click="showDrawer">
                楼栋选择
            </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="选择楼层" 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] // 时间

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')

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

// 光晕效果
const lenPosiotn = new Vector3()
const Plight = new PointLight(dirColor, 0.2, 1500, 0);


const sunDir = new Vector3()  // 太阳光位置
const hlightPosition = new Vector3()  // 背面光位置


const seasons = ['春分', '夏至', '秋分', '冬至']
let SphericalCoords = CHANGSHA[0]
function getSeason(index: number) {
    SphericalCoords = CHANGSHA[index]
    updateSunPostion()

}

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

    // 太阳高度角
    const phi = MathUtils.degToRad(90 - MathUtils.lerp(SphericalCoords[0][curent], SphericalCoords[0][next], index - curent))
    // 太阳方位角
    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)

    // 背面光位置
    hlightPosition.setFromSphericalCoords(800, Math.PI / 2 - phi, Math.PI - theta)
    backlight.position.copy(hlightPosition)

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

    // 直射光
    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>

里面的场景初始化或者循环播放啥的不是核心就不过多的啰嗦啦,看最终效果

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要基于three.js实现3个维度展示柱图,首先需要创建一个three.js的场景,然后在场景中创建一个或多个柱子。每个柱子都需要设置其高度、宽度和深度。接下来,需要为每个柱子创建一个材质,可以使用three.js中的MeshBasicMaterial、MeshLambertMaterial或MeshPhongMaterial等材质类型。然后将柱子和材质绑定起来,创建一个Mesh对象。 为了实现三个维度的展示,可以使用three.js中的坐标系来表示不同的维度。例如,X轴可以表示柱子的宽度,Y轴可以表示柱子的高度,Z轴可以表示柱子的深度。在场景中添加一个坐标轴,并将每个柱子放置在适当的坐标位置上,以便正确地表示其宽度、高度和深度。 最后,需要将场景中的所有对象渲染出来,可以使用three.js中的渲染器来完成。将场景和相机作为参数传递给渲染器的render方法,就可以将场景中的所有对象呈现在浏览器中了。 下面是一个实现三个维度展示柱图的示例代码: ```javascript // 创建一个场景 var scene = new THREE.Scene(); // 创建一个相机 var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.z = 5; // 创建一个渲染器 var renderer = new THREE.WebGLRenderer(); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); // 创建一个坐标轴 var axesHelper = new THREE.AxesHelper( 2 ); scene.add( axesHelper ); // 创建柱子的几何体 var geometry = new THREE.BoxGeometry( 1, 1, 1 ); // 创建柱子的材质 var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); // 创建一个Mesh对象,将柱子和材质绑定起来 var cube = new THREE.Mesh( geometry, material ); // 设置柱子的高度、宽度和深度 cube.scale.x = 0.5; cube.scale.y = 2; cube.scale.z = 0.5; // 将柱子放置在适当的坐标位置上,以便正确地表示其宽度、高度和深度 cube.position.x = -1; cube.position.y = 0; cube.position.z = 0; // 将柱子添加到场景中 scene.add( cube ); // 渲染场景 function animate() { requestAnimationFrame( animate ); renderer.render( scene, camera ); } animate(); ``` 在这个示例代码中,我们创建了一个场景、一个相机、一个渲染器和一个坐标轴。然后创建了一个立方体的几何体和材质,将它们绑定到一个Mesh对象上,并设置了柱子的高度、宽度和深度。最后将柱子添加到场景中,并渲染场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值