【前端转3D】去不了夏威夷,那我就用three实现一个

大家好,我是日拱一卒的攻城师不浪,专注可视化、数字孪生、前端、nodejs、AI学习、GIS等学习沉淀,这是2024年输出的第21/100篇文章;

前言

静态页面写倦了,就来玩玩3D,你会发现又开启了一个新大陆~

好了,废话也不多说了,今天我们来用three从零到一实现一个夏威夷度假海岛,假装自己去了夏威夷...

场景初始化

dart 复制代码
 // 场景
const scene = new THREE.Scene();
 // 初始化相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000)
 // 设置相机位置
camera.position.set(-50, 50, 130);
 // 更新摄像头宽高比例,换句话说就是分辨率
camera.aspect = window.innerWidth / window.innerHeight
 // 更新摄像头投影矩阵
camera.updateProjectionMatrix();
 // 相机添加进场景中
scene.add(camera)

选择three的渲染引擎

这里我们直接选择WebGL渲染引擎;

javascript 复制代码
const renderer = new THREE.WebGLRenderer({
  antialias: true,  // 抗锯齿
  logarithmicDepthBuffer: true,  // 对数深度缓存,提高性能
})
 // 将输出canvas的大小调整为(window.innerWidth, window.innerHeight)并考虑设备像素比
renderer.setSize(window.innerWidth, window.innerHeight)

抗锯齿一定要设置为true,不然绘制出来的几何体会张牙舞爪~

帧渲染动画

很重要 ,three在浏览器里的动画渲染都是经过每一帧进行不断的渲染才形成的,所以用到了浏览器(js)里的帧动画函数requestAnimationFrame

scss 复制代码
const render = () => {
   // 场景渲染
  renderer.render(scene, camera)
  requestAnimationFrame(render)
}

canvas挂载

由于我们是在vue中进行的渲染,所以需要在dom节点渲染完成之后才能将canvas节点进行挂载,所以这些操作都要在onMounted生命周期中进行。

scss 复制代码
<template>
  <div ref="container"</div>
</template>
const container = ref()
onMounted(() => {
  // 添加轨道控制器,可以使得相机围绕目标进行轨道运动
  const controls = new OrbitControls(camera, container.value)
  // 阻尼效果
  controls.enableDamping = true
  container.value.appendChild(renderer.domElement)
  render()
})

绘制天空

把整个场景放置到一个球体当中,形成一个360度环绕视觉感

arduino 复制代码
 // 绘制球体
const skyGeo = new THREE.SphereGeometry(1000, 60, 60);
 // 添加天空材质
const skyTex = new THREE.TextureLoader().load("/images/sky.jpg")
const skyMat = new THREE.MeshBasicMaterial({
  map: skyTex
})
 // 视角进入球体内部
skyGeo.scale(1, 1, -1)
 // 几何体+材质形成一个场景
const sky = new THREE.Mesh(skyGeo, skyMat)
scene.add(sky)

云彩动起来

这里我们先简单搞一下,使用视频去渲染动态云彩和天空,这里的视频也可以作为球体的材质

我们还加了一个控制天空是否动态的按钮,只有点击才播放。

ini 复制代码
 // 视频纹理
const createVideo = () => {
  const video = document.createElement('video')
  video.src = '/video/sky.mp4'
  video.loop = true
  window.addEventListener('click', (e) => {
    if (video.paused) {
      video.play()
      const texture = new THREE.VideoTexture(video)
      skyMat.map = texture
      skyMat.map.needsUpdate = true
    }
  })
}
 // 涉及到dom渲染的,都要在onMounted生命周期执行
onMounted(() => {
   //.... 
  createVideo();
  render();
})

添加海岛模型

使用GLTFLoader 大类,支持加载gltfglb格式类型的三维模型。

javascript 复制代码
 // 添加小岛模型
const loader = new GLTFLoader()
 // 压缩三维模型
const dracoLoader = new DRACOLoader()
 // 添加draco载入库
dracoLoader.setDecoderPath('/js/draco/')
loader.setDRACOLoader(dracoLoader)

loader.load('/glb/island2.glb', (gltf) => {
  const isLand = gltf.scene
  scene.add(isLand)
})

咦?怎么是黑的?

这也是很多three初学者经常会碰到的一个疑问,废了九牛二虎之力才把模型加载到场景中了,发现黑乎乎的一坨,跟在模型师的电脑上看到的完全不一样。

这是因为在three场景中,我们需要手动给场景添加光源,才能照亮场景中的模型等。

添加光源

这里我们直接选择平行光即可,用平行光来模拟太阳光,无限远射。

csharp 复制代码
 // 添加平行光
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(-100, 100, 10)
scene.add(light)

OK,成功渲染模型。

添加水面

我们选择用一个圆形几何体CircleGeometry去构建水面区域。

arduino 复制代码
 // 创建水面
const waterGeo = new THREE.CircleGeometry(300, 64)

几何体构建好之后,要给它上材质,我们直接使用three官方案例里的水材质,已经为我们封装好了

javascript 复制代码
import { Water } from 'three/examples/jsm/objects/Water2'
 //... 
const waterTexLoader = new THREE.TextureLoader()
const waterMat = new Water(waterGeo, {
  textureWidth: 1024,
  textureHeight: 1024,
  color: 0xeeeeff,
  flowDirection: new THREE.Vector2(1, 1),
  scale: 2,
  normalMap0: waterTexLoader.load('/images/Water_1_M_Normal.jpg'),
  normalMap1: waterTexLoader.load('/images/Water_2_M_Normal.jpg')
})

突然发现这水有点上天了...

此时是几何体的位置是垂直的,所以我们要调整它的坐标轴进行旋转,设置为水平方向。

这就又涉及到three轴坐标轴的知识了,在three中,坐标轴遵顼右手定则

相信现在应该有人举起了你的黄金右手了😀

如果还是不知道要怎么旋转整个水面看,新手可以在场景中添加坐标轴辅助。

csharp 复制代码
scene.add(new THREE.AxesHelper(1000))

这下很明显了把,我们是不是应该绕着X轴,让它逆时针旋转90度呢。

ini 复制代码
waterMat.rotation.x = -Math.PI / 2

OK,为了让场景效果更逼真一点,我们可以把水面稍稍太高,让石头若隐若现,呈现一种朦胧美。

arduino 复制代码
 // 水平面抬高3米淹没石头
waterMat.position.y = 3

好的,已经有那个感觉了。

但是,还是有一点点瑕疵,仔细观察一下,椰子树的树叶下边,也就是背光的地方,黑乎乎的一片,显得有点格格不入了。

这也难不倒我们,使用环境纹理去优化它!

javascript 复制代码
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'

 // 载入环境纹理
const hdrLoader = new RGBELoader()
hdrLoader.loadAsync('/hdr/050.hdr').then((texture) => {
   // 材质映射
  texture.mapping = THREE.EquirectangularReflectionMapping
  scene.background = texture
  scene.environment = texture
})

THREE.EquirectangularReflectionMapping :一个材质映射模式,用于将纹理映射到3D对象上。这种映射方式特别适用于环境映射,即反射映射,它可以创建出非常逼真的反射效果。

注意 :使用全景纹理可能会对性能有所影响,因为它需要对纹理进行较多的处理来适应3D模型的表面。

这下是不是就感觉舒服多了,这场景有点类似于我的世界里的场景一隅了。

【开源地址】:github.com/day-day-dre...

有需要进技术产品开发交流群(可视化&GIS)可以加我:brown_7778,也欢迎数字孪生可视化领域的交流合作。

最后,如果觉得文章对你有帮助,也希望可以一键三连👏👏👏,支持我持续开源和分享~

相关推荐
go2coding13 分钟前
开源 复刻GPT-4o - Moshi;自动定位和解决软件开发中的问题;ComfyUI中使用MimicMotion;自动生成React前端代码
前端·react.js·前端框架
freesharer33 分钟前
Zabbix 配置WEB监控
前端·数据库·zabbix
web前端神器33 分钟前
forever启动后端服务,自带日志如何查看与设置
前端·javascript·vue.js
是Yu欸39 分钟前
【前端实现】在父组件中调用公共子组件:注意事项&逻辑示例 + 将后端数组数据格式转换为前端对象数组形式 + 增加和删除行
前端·vue.js·笔记·ui·vue
今天是 几 号1 小时前
WEB攻防-XSS跨站&反射型&存储型&DOM型&标签闭合&输入输出&JS代码解析
前端·javascript·xss
A-超1 小时前
html5 video去除边框
前端·html·html5
进击的阿三姐1 小时前
vue2项目迁移vue3与gogocode的使用
前端·javascript·vue.js
hawk2014bj2 小时前
React 打包时如何关闭源代码混淆
前端·react.js·前端框架
不会倒的鸡蛋2 小时前
网络爬虫详解
前端·chrome·python
CiL#2 小时前
vue2+element-ui新增编辑表格+删除行
前端·vue.js·elementui