效果预览:
项目已经开源,包含部分注释,如果对你有帮助,欢迎start。github.com/IrisPro/3d-…
功能介绍
- 实时查看一个建筑物在某一天的某个时刻的光照情况
- 可视化地查看建筑日照时长的情况
核心实现
当前仅列出核心步骤,具体实现欢迎查看源码
技术栈:
- threejs
- vue3
- vantUI
- suncalc.js: 通过经纬度和位置,计算太阳运行的轨迹
素材准备
- 背景图:我的图片一张地图
- 普通模型:glb、gltf、obj等格式都可以,我的模型是自己使用sketchup建模的
- 具有颜色的模型(可选):如上图所示的日照时长,我使用日照大师做出来的
部分实现
threejs相关初始化
...
init() {
this.initScene() // 初始化场景
this.initCamera() // 初始化相机
this.initRender() // 初始化渲染器
this.orbitHelper() // 轨道控制器
this.statsHelper() //性能辅助
this.animate() // 动画
window.onresize = this.onWindowResize.bind(this) // 监听屏幕变化
}
// 加载模型:解码、设置物体接受阴影
loadModel() {
const loader = new GLTFLoader()
/**
* DRACOLoader 解码器介绍:
* 在Three.js中,加载GLB模型时是否需要DRACOLoader解码器取决于您的模型文件是否使用了Draco压缩。
* DRACOLoader是一个用于解码Draco压缩网格数据的加载器。
* 如果您的GLB模型文件中的几何体数据已经通过 Draco 压缩过,那么在加载时就需要使用DRACOLoader进行解码。
* 如果没有进行Draco压缩,那么直接使用GLTFLoader即可加载,无需额外设置DRACOLoader。
*/
/**
* 如果您使用gltf-pipeline工具对模型进行了Draco压缩,
* 那么在加载这个压缩后的GLTF或GLB文件时,
* 就需要用到DRACOLoader来解码Draco压缩过的几何数据。
*/
const dracoLoader = new DRACOLoader()
/**
* 设置Draco解码库
* Path: node_modules/three/examples/jsm/libs/draco文件复制到public文件下
*/
dracoLoader.setDecoderPath('./draco/')
dracoLoader.setDecoderConfig({ type: 'js' }) // 使用js方式解压
dracoLoader.preload() // 初始化_initDecoder 解码器
loader.setDRACOLoader(dracoLoader) // 设置gltf加载器dracoLoader解码器
loader.load(
this.modelUrl,
(gltf) => {
const model = gltf.scene
// 遍历模型中的所有子对象,设置阴影接收和投射属性
model.traverse((child) => {
if (child.isMesh) {
const copyMaterial = child.material.clone()
copyMaterial.side = THREE.DoubleSide
copyMaterial.originColor = copyMaterial.color.clone()
copyMaterial.color.setHex(0xfffff0)
child.material = copyMaterial
//物体遮挡阴影
child.castShadow = true
child.receiveShadow = true
}
})
model.scale.set(this.modelScale, this.modelScale, this.modelScale)
this.building = gltf.scene
this.scene.add(gltf.scene)
},
undefined,
function (error) {
console.error(error)
},
)
}
// 添加自然光、太阳光
...
// 半球光,会比环境光颜色更自然
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 1) // (sky color, floor color)
this.scene.add(hemisphereLight)
...
initLight() {
this.sunLight = new THREE.DirectionalLight(0xffffff)
this.sunLight.visible = true
this.sunLight.intensity = 20 //光线的密度,默认为1。 光照越强,物体表面就更明亮
this.sunLight.shadow.camera.near = -1000 //产生阴影的最近距离
this.sunLight.shadow.camera.far = 1000 //产生阴影的最远距离
this.sunLight.shadow.camera.left = -1000 //产生阴影距离位置的最左边位置
this.sunLight.shadow.camera.right = 1000 //最右边
this.sunLight.shadow.camera.top = 1000 //最上边
this.sunLight.shadow.camera.bottom = -1000 //最下面
this.sunLight.shadow.bias = -0.01 //用于解决阴影水波纹条纹阴影的问题
this.sunLight.shadow.mapSize.set(4096, 4096) //阴影清晰度
//告诉平行光需要开启阴影投射,物体遮挡阴影
this.sunLight.castShadow = true
this.scene.add(this.sunLight)
}
//
...
// 省略...... 感兴趣可以查看源码
通过经纬度和位置,计算太阳运行的轨迹
// 计算太阳的位置
const calcSunPosition = () => {
const sunPosition = SunCalc.getPosition(
curDate.value,
latitude.value,
longitude.value,
)
// 太阳角度偏移(可选)
const offSetRad =
sunOffset.value > 0 ? THREE.MathUtils.degToRad(sunOffset.value) : 0
const sunDirection = new THREE.Vector3()
sunDirection.setFromSphericalCoords(
1,
Math.PI / 2 - sunPosition.altitude,
-sunPosition.azimuth - offSetRad,
)
sunDirection.normalize()
//光源到原点的距离
sunlightPosition.value = sunDirection.clone().multiplyScalar(200)
}
创建标签
/**
* 创建标签示例
*/
import * as THREE from 'three'
import { labelRenderer as labelRenderer2D, tag as tag2D } from '@/core/tag2D'
import { ref } from 'vue'
export const useTag = (threeObj) => {
const labelRenderer2DObj = ref(null)
const handleClickTag = (e) => {
console.log('点击了', e.target.innerHTML)
}
const createTag = (list = []) => {
const tagList = [
{
className: 'tag',
modelName: '可选(自定义属性)',
name: '1栋',
position: new THREE.Vector3(14.163, 16.252, 141.966),
},
{
className: 'tag',
modelName: 'xxx',
name: '2栋',
position: new THREE.Vector3(3.216, 74.810, 103.162),
},
...
]
tagList.forEach(({ name, position, modelName, className }) => {
const label2D = tag2D(name, modelName, className, handleClickTag)
position.multiplyScalar(threeObj.modelScale)
label2D.position.copy(position)
threeObj.scene.add(label2D)
})
if (!labelRenderer2DObj.value) {
labelRenderer2DObj.value = labelRenderer2D(threeObj.container)
}
return labelRenderer2DObj
}
const renderTag = (scene, camera) => {
return labelRenderer2DObj.value?.render(
scene ?? threeObj.scene,
camera ?? threeObj.camera,
)
}
return {
createTag,
renderTag,
}
}