声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

背景

深居内陆的人们,大概每个人都有过大海之梦吧。夏日傍晚在沙滩漫步奔跑;或是在海上冲浪游泳;或是在海岛游玩探险;亦或静待日出日落……本文使用 React + Three.js 技术栈,实现 3D 海洋和岛屿,主要包含知识点包括:Tone MappingWater 类、Sky 类、Shader 着色、ShaderMaterial 着色器材质、Raycaster 检测遮挡以及 Three.js 的其他基础知识,让我们在这个夏天通过此页面共赴大海之约。

效果

实现

‍ 素材准备

开发之前,需要准备页面所需的素材,本文用到的海岛素材是在 sketchfab.com 找的免费模型。下载好素材之后,在 Blender 中打开,按自己的想法调整模型的颜色、材质、大小比例、角度、位置等信息,删减不需要的模块、缩减面数以压缩模型体积,最后删除相机、光照、UV、动画等多余信息,只导出模型网格备用。

资源引入

首先,引入开发所需的必备资源,OrbitControls 用于镜头轨道控制;GLTFLoader 用于加载 gltf 格式模型;WaterThree.js 内置的一个类,可以生成类似水的效果;Sky 可以生成天空效果;TWEEN 用来生成补间动画;Animations 是对 TWEEN 控制镜头补间动画方法的封装;waterTextureflamingoModelislandModel 三者分别是水的法向贴图、飞鸟模型、海岛模型;vertexShaderfragmentShader 是用于生成彩虹的 Shader 着色器。

  1. import * as THREE from "three";
  2. import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
  3. import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
  4. import { Water } from 'three/examples/jsm/objects/Water';
  5. import { Sky } from 'three/examples/jsm/objects/Sky';
  6. import { TWEEN } from "three/examples/jsm/libs/tween.module.min";
  7. import Animations from '@/assets/utils/animations';
  8. import waterTexture from '@/containers/Ocean/images/waternormals.jpg';
  9. import islandModel from '@/containers/Ocean/models/island.glb';
  10. import flamingoModel from '@/containers/Ocean/models/flamingo.glb';
  11. import vertexShader from '@/containers/Ocean/shaders/rainbow/vertex.glsl';
  12. import fragmentShader from '@/containers/Ocean/shaders/rainbow/fragment.glsl';

页面结构

页面主要由3部分构成:canvas.webgl 用于渲染 WEBGL 场景;div.loading 用于模型加载完成前显示加载进度;div.point 用于添加交互点,省略部分是其他几个交互点信息。

  1. render () {
  2. return (
  3. <div className='ocean'>
  4. <canvas className='webgl'></canvas>
  5. {this.state.loadingProcess === 100 ? '' : (
  6. <div className='loading'>
  7. <span className='progress'>{this.state.loadingProcess} %</span>
  8. </div>
  9. )}
  10. <div className="point point-0">
  11. <div className="label label-0">1</div>
  12. <div className="text">灯塔:矗立在海岸的岩石之上,白色的塔身以及红色的塔屋,在湛蓝色的天空和深蓝色大海的映衬下,显得如此醒目和美丽。</div>
  13. </div>
  14. // ...
  15. </div>
  16. )
  17. }

场景初始化

在这部分,先定义好需要的状态值,loadingProcess 用于显示页面加载进度。

  1. state = {
  2. loadingProcess: 0
  3. }

定义一些全局变量和参数,初始化场景、相机、镜头轨道控制器、灯光、页面缩放监听等。

  1. const clock = new THREE.Clock();
  2. const raycaster = new THREE.Raycaster()
  3. const sizes = {
  4. width: window.innerWidth,
  5. height: window.innerHeight
  6. }
  7. const renderer = new THREE.WebGLRenderer({
  8. canvas: document.querySelector('canvas.webgl'),
  9. antialias: true
  10. });
  11. renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  12. renderer.setSize(sizes.width, sizes.height);
  13. // 设置渲染效果
  14. renderer.toneMapping = THREE.ACESFilmicToneMapping;
  15. // 创建场景
  16. const scene = new THREE.Scene();
  17. // 创建相机
  18. const camera = new THREE.PerspectiveCamera(55, sizes.width / sizes.height, 1, 20000);
  19. camera.position.set(0, 600, 1600);
  20. // 添加镜头轨道控制器
  21. const controls = new OrbitControls(camera, renderer.domElement);
  22. controls.target.set(0, 0, 0);
  23. controls.enableDamping = true;
  24. controls.enablePan = false;
  25. controls.maxPolarAngle = 1.5;
  26. controls.minDistance = 50;
  27. controls.maxDistance = 1200;
  28. // 添加环境光
  29. const ambientLight = new THREE.AmbientLight(0xffffff, .8);
  30. scene.add(ambientLight);
  31. // 添加平行光
  32. const dirLight = new THREE.DirectionalLight(0xffffff, 1);
  33. dirLight.color.setHSL(.1, 1, .95);
  34. dirLight.position.set(-1, 1.75, 1);
  35. dirLight.position.multiplyScalar(30);
  36. scene.add(dirLight);
  37. // 页面缩放监听并重新更新场景和相机
  38. window.addEventListener('resize', () => {
  39. camera.aspect = window.innerWidth / window.innerHeight;
  40. camera.updateProjectionMatrix();
  41. renderer.setSize(window.innerWidth, window.innerHeight);
  42. }, false);

Tone Mapping

可以注意到,本文使用了 renderer.toneMapping = THREE.ACESFilmicToneMapping 来设置页面渲染效果。目前 Three.js 中有以下几种 Tone Mapping 值,它们定义了 WebGLRenderertoneMapping 属性,用于在近似标准计算机显示器或移动设备的低动态范围 LDR 屏幕上展示高动态范围 HDR 外观。大家可以修改不同的值看看渲染效果有何不同。

  • THREE.NoToneMapping
  • THREE.LinearToneMapping
  • THREE.ReinhardToneMapping
  • THREE.CineonToneMapping
  • THREE.ACESFilmicToneMapping

使用 Three.js 自带的 Water 类创建海洋,首先创建一个平面网格 waterGeometry,让后将它传递给 Water,并配置相关属性,最后将海洋添加到场景中。

  1. const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
  2. const water = new Water(waterGeometry, {
  3. textureWidth: 512,
  4. textureHeight: 512,
  5. waterNormals: new THREE.TextureLoader().load(waterTexture, texture => {
  6. texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  7. }),
  8. sunDirection: new THREE.Vector3(),
  9. sunColor: 0xffffff,
  10. waterColor: 0x0072ff,
  11. distortionScale: 4,
  12. fog: scene.fog !== undefined
  13. });
  14. water.rotation.x = - Math.PI / 2;
  15. scene.add(water);

Water 类

参数说明

  • textureWidth:画布宽度
  • textureHeight:画布高度
  • waterNormals:法向量贴图
  • sunDirection:阳光方向
  • sunColor:阳光颜色
  • waterColor:水颜色
  • distortionScale:物体倒影分散度
  • fog:雾
  • alpha:透明度

接着,使用 Three.js 自带的天空类 Sky 创建天空,通过修改着色器参数设置天空样式,然后创建太阳并添加到场景中。

  1. const sky = new Sky();
  2. sky.scale.setScalar(10000);
  3. scene.add(sky);
  4. const skyUniforms = sky.material.uniforms;
  5. skyUniforms['turbidity'].value = 20;
  6. skyUniforms['rayleigh'].value = 2;
  7. skyUniforms['mieCoefficient'].value = 0.005;
  8. skyUniforms['mieDirectionalG'].value = 0.8;
  9. // 太阳
  10. const sun = new THREE.Vector3();
  11. const pmremGenerator = new THREE.PMREMGenerator(renderer);
  12. const phi = THREE.MathUtils.degToRad(88);
  13. const theta = THREE.MathUtils.degToRad(180);
  14. sun.setFromSphericalCoords(1, phi, theta);
  15. sky.material.uniforms['sunPosition'].value.copy(sun);
  16. water.material.uniforms['sunDirection'].value.copy(sun).normalize();
  17. scene.environment = pmremGenerator.fromScene(sky).texture;

Sky 类

天空材质着色器参数说明

  • turbidity 浑浊度
  • rayleigh 视觉效果就是傍晚晚霞的红光的深度
  • luminance 视觉效果整体提亮或变暗
  • mieCoefficient 散射系数
  • mieDirectionalG 定向散射值

首先,创建具有彩虹渐变效果的着色器 Shader, 然后使用着色器材质 ShaderMaterial, 创建圆环 THREE.TorusGeometry 并添加到场景中。

顶点着色器 vertex.glsl

  1. varying vec2 vUV;
  2. varying vec3 vNormal;
  3. void main () {
  4. vUV = uv;
  5. vNormal = vec3(normal);
  6. gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  7. }

片段着色器 fragment.glsl

  1. varying vec2 vUV;
  2. varying vec3 vNormal;
  3. void main () {
  4. vec4 c = vec4(abs(vNormal) + vec3(vUV, 0.0), 0.1); // 设置透明度为0.1
  5. gl_FragColor = c;
  6. }

彩虹渐变着色器效果

  1. const material = new THREE.ShaderMaterial({
  2. side: THREE.DoubleSide,
  3. transparent: true,
  4. uniforms: {},
  5. vertexShader: vertexShader,
  6. fragmentShader: fragmentShader
  7. });
  8. const geometry = new THREE.TorusGeometry(200, 10, 50, 100);
  9. const torus = new THREE.Mesh(geometry, material);
  10. torus.opacity = .1;
  11. torus.position.set(0, -50, -400);
  12. scene.add(torus);

Shader 着色器

WebGL 中记述了坐标变换的机制就叫做着色器 Shader,着色器又有处理几何图形顶点的 顶点着色器 和处理像素的 片段着色器 两种类型

准备顶点着色器和片元着色器

着色器的添加有多种方法,最简单的方法就是把着色器记录在 HTML 中。该方法利用HTMLscript 标签来实现,如:

顶点着色器

  1. <script id="vshader" type="x-shader/x-vertex"></script>

片段着色器

  1. <script id="fshader" type="x-shader/x-fragment"></script>

也可以像本文中一样,直接使用单独创建 glsl 格式文件引入。

着色器的三个变量与运行方式
  • Uniforms:是所有顶点都具有相同的值的变量。 比如灯光,雾,和阴影贴图就是被储存在 uniforms 中的数据。uniforms 可以通过顶点着色器和片元着色器来访问。
  • Attributes:是与每个顶点关联的变量。例如,顶点位置,法线和顶点颜色都是存储在 attributes 中的数据。attributes 只可以在顶点着色器中访问。
  • Varyings:是从顶点着色器传递到片元着色器的变量。对于每一个片元,每一个varying 的值将是相邻顶点值的平滑插值。

顶点着色器 首先运行,它接收 attributes, 计算每个单独顶点的位置,并将其他数据varyings 传递给片段着色器。片段着色器 后运行,它设置渲染到屏幕的每个单独的片段的颜色。

ShaderMaterial 着色器材质

Three.js 所谓的材质对象 Material 本质上就是着色器代码和需要传递的 uniform 数据光源、颜色、矩阵Three.js 提供可直接渲染着色器语法的材质 ShaderMaterialRawShaderMaterial

  • RawShaderMaterial: 和原生 WebGL 中一样,顶点着色器、片元着色器代码基本没有任何区别,不过顶点数据和 uniform 数据可以通过 Three.jsAPI 快速传递,要比使用 WebGL 原生的 API 与着色器变量绑定要方便得多。
  • ShaderMaterialShaderMaterialRawShaderMaterial 更方便些,着色器中的很多变量不用声明,Three.js 系统会自动设置,比如顶点坐标变量、投影矩阵、视图矩阵等。

构造函数

  1. ShaderMaterial(parameters : Object)

parameters:可选,用于定义材质外观的对象,具有一个或多个属性。

常用属性

  • attributes[Object]:接受如下形式的对象,{ attribute1: { value: []} } 指定要传递给顶点着色器代码的 attributes;键为 attribute 修饰变量的名称,值也是对象格式,如 { value: [] }value 是固定名称,因为 attribute 相对于所有顶点,所以应该回传一个数组格式。只有 bufferGeometry 类型的能使用该属性。
  • .uniforms[Object]:如下形式的对象:{ uniform1: { value: 1.0 }, uniform2: { value: 2.0 }} 指定要传递给shader 代码的 uniforms;键为 uniform 的名称,值是如下形式:{ value: 1.0 } 这里 valueuniform 的值。名称必须匹配着色器代码中 uniformname,和 GLSL 代码中的定义一样。 注意,uniforms 逐帧被刷新,所以更新 uniform 值将立即更新 GLSL 代码中的相应值。
  • .fragmentShader[String]:片元着色器的 GLSL 代码,它也可以作为一个字符串直接传递或者通过 AJAX 加载。
  • .vertexShader[String]:顶点着色器的 GLSL 代码,它也可以作为一个字符串直接传递或者通过 AJAX 加载。

接着,使用 GLTFLoader 加载岛屿模型并添加到场景中。加载之前可以使用 LoadingManager 来管理加载进度。

  1. const manager = new THREE.LoadingManager();
  2. manager.onProgress = async(url, loaded, total) => {
  3. if (Math.floor(loaded / total * 100) === 100) {
  4. this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
  5. Animations.animateCamera(camera, controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 4000, () => {
  6. this.setState({ sceneReady: true });
  7. });
  8. } else {
  9. this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
  10. }
  11. };
  12. const loader = new GLTFLoader(manager);
  13. loader.load(islandModel, mesh => {
  14. mesh.scene.traverse(child => {
  15. if (child.isMesh) {
  16. child.material.metalness = .4;
  17. child.material.roughness = .6;
  18. }
  19. })
  20. mesh.scene.position.set(0, -2, 0);
  21. mesh.scene.scale.set(33, 33, 33);
  22. scene.add(mesh.scene);
  23. });

使用 GLTFLoader 加载岛屿模型添加到场景中,获取模型自带的动画帧并进行播放,记得要在 requestAnimationFrame 中更新动画。可以使用 clone 方法在场景中添加多只飞鸟。鸟模型来源于 Three.js 官网。

  1. loader.load(flamingoModel, gltf => {
  2. const mesh = gltf.scene.children[0];
  3. mesh.scale.set(.35, .35, .35);
  4. mesh.position.set(-100, 80, -300);
  5. mesh.rotation.y = - 1;
  6. mesh.castShadow = true;
  7. scene.add(mesh);
  8. const mixer = new THREE.AnimationMixer(mesh);
  9. mixer.clipAction(gltf.animations[0]).setDuration(1.2).play();
  10. this.mixers.push(mixer);
  11. });

交互点

添加交互点,鼠标 hover 悬浮时显示提示语,点击交互点可以切换镜头角度,视角聚焦到交互点对应的位置 上。

  1. const points = [
  2. {
  3. position: new THREE.Vector3(10, 46, 0),
  4. element: document.querySelector('.point-0')
  5. },
  6. // ...
  7. ];
  8. document.querySelectorAll('.point').forEach(item => {
  9. item.addEventListener('click', event => {
  10. let className = event.target.classList[event.target.classList.length - 1];
  11. switch(className) {
  12. case 'label-0':
  13. Animations.animateCamera(camera, controls, { x: -15, y: 80, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
  14. break;
  15. // ...
  16. }
  17. }, false);
  18. });

动画

requestAnimationFrame 中更新水、镜头轨道控制器、相机、TWEEN、交互点等动画。

  1. const animate = () => {
  2. requestAnimationFrame(animate);
  3. water.material.uniforms['time'].value += 1.0 / 60.0;
  4. controls && controls.update();
  5. const delta = clock.getDelta();
  6. this.mixers && this.mixers.forEach(item => {
  7. item.update(delta);
  8. });
  9. const timer = Date.now() * 0.0005;
  10. TWEEN && TWEEN.update();
  11. camera && (camera.position.y += Math.sin(timer) * .05);
  12. if (this.state.sceneReady) {
  13. // 遍历每个点
  14. for (const point of points) {
  15. // 获取2D屏幕位置
  16. const screenPosition = point.position.clone();
  17. screenPosition.project(camera);
  18. raycaster.setFromCamera(screenPosition, camera);
  19. const intersects = raycaster.intersectObjects(scene.children, true);
  20. if (intersects.length === 0) {
  21. // 未找到相交点,显示
  22. point.element.classList.add('visible');
  23. } else {
  24. // 找到相交点
  25. // 获取相交点的距离和点的距离
  26. const intersectionDistance = intersects[0].distance;
  27. const pointDistance = point.position.distanceTo(camera.position);
  28. // 相交点距离比点距离近,隐藏;相交点距离比点距离远,显示
  29. intersectionDistance < pointDistance ? point.element.classList.remove('visible') : point.element.classList.add('visible');
  30. }
  31. const translateX = screenPosition.x * sizes.width * 0.5;
  32. const translateY = - screenPosition.y * sizes.height * 0.5;
  33. point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
  34. }
  35. }
  36. renderer.render(scene, camera);
  37. }
  38. animate();
  39. }

Raycaster 检测遮挡

仔细观察,在上述 更新交互点动画的方法中,通过 raycaster 射线来检查交互点是否被物体遮挡,如果被遮挡就隐藏交互点,否则显示交互点,大家可以通过旋转场景观察到这一效果。

总结

本文包含的新知识点主要包括:

  • Tone Mapping
  • Water
  • Sky
  • Shader 着色器
  • ShaderMaterial 着色器材质
  • Raycaster 检测遮挡

想了解其他前端知识或其他未在本文中详细描述的 Web 3D 开发技术相关知识,可阅读我往期的文章。转载请注明原文地址和作者。如果觉得文章对你有帮助,不要忘了一键三连哦

参考

附录

本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/16316217.html

Three.js 打造缤纷夏日3D梦中情岛 🌊的更多相关文章

  1. HTML5,不仅仅是看上去非常美(第二弹:打造最美3D机房)

    前言 近期项目开发任务告一段落,刚好有时间整理这大半年的一些成果.使用html5时间还不久,对js的认识还不够深入.没办法,曾经一直搞java,对js的一些语言特性和概念一时还转换只是来. 上一篇第一 ...

  2. JS打造的跟随鼠标移动的酷炫拓扑图案

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  3. js矩阵菜单或3D立体预览图片效果

    js矩阵菜单或3D立体预览图片效果 下载地址: http://files.cnblogs.com/elves/js%E7%9F%A9%E9%98%B5%E8%8F%9C%E5%8D%95%E6%88% ...

  4. 用webgl打造自己的3D迷宫游戏

    用webgl打造自己的3D迷宫游戏 2016/09/19 · JavaScript · WebGL 原文出处: AlloyTeam    背景:前段时间自己居然迷路了,有感而发就想到写一个可以让人迷路 ...

  5. 你要是还学不会,请提刀来见 Typora+PicGo+Gitee + node.js 打造个人高效稳定优雅图床

    你要是还学不会,请提刀来见 Typora+PicGo+Gitee + node.js 打造个人高效稳定优雅图床 经过前面两弹的介绍,相信大家对图床都不陌生了吧, 但是小魔童觉得这样做法还是不方便,使用 ...

  6. 1000粉!使用Three.js制作一个专属3D奖牌🥇

    背景 破防了 !突然发现 SegmentFault 平台的粉丝数量已经突破 1000 了,它是我的三个博客平台掘金.博客园.SegmentFault中首个粉丝突破 1000 的,于是设计开发这个页面, ...

  7. 使用Three.js实现神奇的3D文字悬浮效果

    声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 背景 在 Three.js Journey 课程示例中,提供了一个使用 Thre ...

  8. 三分钟学会用 js + css3 打造酷炫3D相册

    之前发过该文,后来不知怎么回事不见了,现在重新发一下. 中秋主题的3D旋转相册 如图,这是通过Javascript和css3来实现的.整个案例只有不到80行代码,我希望通过这个案例,让正处于迷茫期的j ...

  9. GJM :JS + CSS3 打造炫酷3D相册 [转载]

    感谢您的阅读.喜欢的.有用的就请大哥大嫂们高抬贵手"推荐一下"吧!你的精神支持是博主强大的写作动力以及转载收藏动力.欢迎转载! 版权声明:本文原创发表于 [请点击连接前往] ,未经 ...

随机推荐

  1. python计算项目净现值和内部回报率

     代码: import numpy as np from numpy import irr import warnings def project(number, period_list): rate ...

  2. 【Android开发】用WebView访问证书有问题的SSL网页

    Android系统的碎片化很严重,并且手机日期不正确.手机根证书异常.com.google.android.webview BUG等各种原因,都会导致WebViewClient无法访问HTTPS站点. ...

  3. jboss学习4-jboss7开发配置指南

    1      Jboss7下载与安装1.1     官方下载路径:http://www.jboss.org/jbossas/downloads,目前最新稳定版本为7.1.1 final,分别有zip和 ...

  4. ES 架构及基础 - 1

    Elasticsearch 是一款分布式,RESTful 风格的搜索和数据分析引擎,可以从海量的数据中高效的找到相关信息.如 wiki 用 ES 进行全文检索及其高亮,Github 用其检索代码,电商 ...

  5. FastAPI(六十八)实战开发《在线课程学习系统》接口开发--用户 个人信息接口开发

    在之前的文章:FastAPI(六十七)实战开发<在线课程学习系统>接口开发--用户登陆接口开发,今天实战:用户 个人信息接口开发. 在开发个人信息接口的时候,我们要注意了,因为我们不一样的 ...

  6. 开源.net core 验证码 - LazyCaptcha

    LazyCaptcha 介绍 LazyCaptcha是仿EasyCaptcha和SimpleCaptcha,基于.Net Standard 2.1的图形验证码模块. 项目地址 效果展示 Captcha ...

  7. spring配置数据源(交给spring容器完成)

    ##将DataSource的创建权交给spring容器去完成 1.导入spring依赖 <dependency> <groupId>org.springframework< ...

  8. python解释器安装与使用

    Python解释器安装与使用 首先了解下python是由'龟叔' 也就是右边这位和蔼的大叔叔 全名'Guido van Rossum'在1989年圣诞节期间,为了打发无聊的圣诞节而编写的一个编程语言. ...

  9. 帝国cms一键排版删除段落前两个空格和换行符

    打开网站根目录下e\class\function.php文件:一.删除两个空格: 搜索:$nbsp=' '; 改为:$nbsp=''; 二.删除<br>换行符 搜索: $ok='<b ...

  10. 3.SRE.操作手册:基础篇

    SRE的根基起码应该包括:SLO.监控.告警.减少琐事和简单化. SLO(服务质量目标):用于描述服务可靠性的程度. SRE的职责并不只是将"所有工作"都自动化,并保持" ...