Disclaimer: The graphic and model materials involved in this article are only for personal study, research and appreciation. Please do not re-modify, illegally spread, reprint, publish, commercialize, or conduct other profit-making activities.
background
People living deep in the inland, probably everyone has a dream of the sea. Walking and running on the beach in the summer evening; or React + Three.js
and swimming in the sea; or 3D
the island; 3D
海洋和岛屿,主要包含知识点包括: Tone Mapping
、 Water
类、 Sky
类、 Shader
着色、 ShaderMaterial
Shader materials, Raycaster
detecting occlusions and other basics of Three.js
let's go to the sea this summer through this page.
Effect
-
💻
This page is only suitablePC
end, the large screen access effect is better. -
👁🗨
Online preview address 1: https://3d-eosin.vercel.app/#/ocean -
👁🗨
Online preview address 2: https://dragonir.github.io/3d/#/ocean
accomplish
👨🎨 Material preparation
Before development, you need to prepare the materials required for the page. The island materials used in this article are free models found on sketchfab.com . After downloading the material, open it in Blender
, adjust the color, material, size ratio, angle, position and other information of the model according to your own ideas, delete unnecessary modules, reduce the number of faces to compress the model volume , and finally delete redundant information such as camera, lighting, UV
, animation, etc., and only export the model mesh for backup.
📦 Resource introduction
First, introduce the necessary resources required for development, OrbitControls
for lens track control; GLTFLoader
for loading --- Water
gltf
format model; Water
Three.js
a built-in class that can generate water-like effects; Sky
can generate sky effects; TWEEN
used to generate tween animations; Animations
TWEEN
补间动画方法的封装; waterTexture
、 flamingoModel
、 islandModel
水vertexShader
and fragmentShader
are used to generate rainbow Shader
shader.
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { Water } from 'three/examples/jsm/objects/Water';
import { Sky } from 'three/examples/jsm/objects/Sky';
import { TWEEN } from "three/examples/jsm/libs/tween.module.min";
import Animations from '@/assets/utils/animations';
import waterTexture from '@/containers/Ocean/images/waternormals.jpg';
import islandModel from '@/containers/Ocean/models/island.glb';
import flamingoModel from '@/containers/Ocean/models/flamingo.glb';
import vertexShader from '@/containers/Ocean/shaders/rainbow/vertex.glsl';
import fragmentShader from '@/containers/Ocean/shaders/rainbow/fragment.glsl';
📃 Page Structure
页面主要由3部分构成: canvas.webgl
WEBGL
场景; div.loading
前显示加载进度; div.point
Used to add interaction points, the omitted part is the information of several other interaction points.
render () {
return (
<div className='ocean'>
<canvas className='webgl'></canvas>
{this.state.loadingProcess === 100 ? '' : (
<div className='loading'>
<span className='progress'>{this.state.loadingProcess} %</span>
</div>
)}
<div className="point point-0">
<div className="label label-0">1</div>
<div className="text">灯塔:矗立在海岸的岩石之上,白色的塔身以及红色的塔屋,在湛蓝色的天空和深蓝色大海的映衬下,显得如此醒目和美丽。</div>
</div>
// ...
</div>
)
}
🌏 Scene initialization
In this part, first define the required status value, loadingProcess
is used to display the page loading progress.
state = {
loadingProcess: 0
}
Define some global variables and parameters, initialize the scene, camera, lens track controller, light, page zoom monitor, etc.
const clock = new THREE.Clock();
const raycaster = new THREE.Raycaster()
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('canvas.webgl'),
antialias: true
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setSize(sizes.width, sizes.height);
// 设置渲染效果
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(55, sizes.width / sizes.height, 1, 20000);
camera.position.set(0, 600, 1600);
// 添加镜头轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.enablePan = false;
controls.maxPolarAngle = 1.5;
controls.minDistance = 50;
controls.maxDistance = 1200;
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, .8);
scene.add(ambientLight);
// 添加平行光
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.color.setHSL(.1, 1, .95);
dirLight.position.set(-1, 1.75, 1);
dirLight.position.multiplyScalar(30);
scene.add(dirLight);
// 页面缩放监听并重新更新场景和相机
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
💡
Tone Mapping
It can be noticed that this article uses renderer.toneMapping = THREE.ACESFilmicToneMapping
to set the page rendering effect. Currently Three.js
there are the following Tone Mapping
values, which define the WebGLRenderer
the toneMapping
property for the approximate standard computer display Low dynamic range LDR
high dynamic range HDR
appearance on the mobile device. You can modify different values to see how the rendering effect is different.
-
THREE.NoToneMapping
-
THREE.LinearToneMapping
-
THREE.ReinhardToneMapping
-
THREE.CineonToneMapping
-
THREE.ACESFilmicToneMapping
🌊 sea
Three.js
自带的Water
类创建海洋,首先创建一个平面网格waterGeometry
,让后将它传递给Water
, And configure the relevant properties, and finally add the ocean to the scene.
const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
const water = new Water(waterGeometry, {
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load(waterTexture, texture => {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}),
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x0072ff,
distortionScale: 4,
fog: scene.fog !== undefined
});
water.rotation.x = - Math.PI / 2;
scene.add(water);
💡
Water class
Parameter description :
-
textureWidth
: canvas width -
textureHeight
: canvas height -
waterNormals
: normal vector map -
sunDirection
: Sunlight direction -
sunColor
: sunlight color -
waterColor
: water color -
distortionScale
: Object reflection dispersion -
fog
: fog -
alpha
: Transparency
🌞 empty
Next, use the sky class that comes with Three.js
Sky
to create the sky, set the sky style by modifying the shader parameters, and then create the sun and add it to the scene.
const sky = new Sky();
sky.scale.setScalar(10000);
scene.add(sky);
const skyUniforms = sky.material.uniforms;
skyUniforms['turbidity'].value = 20;
skyUniforms['rayleigh'].value = 2;
skyUniforms['mieCoefficient'].value = 0.005;
skyUniforms['mieDirectionalG'].value = 0.8;
// 太阳
const sun = new THREE.Vector3();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
const phi = THREE.MathUtils.degToRad(88);
const theta = THREE.MathUtils.degToRad(180);
sun.setFromSphericalCoords(1, phi, theta);
sky.material.uniforms['sunPosition'].value.copy(sun);
water.material.uniforms['sunDirection'].value.copy(sun).normalize();
scene.environment = pmremGenerator.fromScene(sky).texture;
💡
Sky class
Description of sky material shader parameters :
-
turbidity
turbidity -
rayleigh
The visual effect is the depth of the red light of the evening sunset -
luminance
overall visual effect is brightened or darkened -
mieCoefficient
scattering coefficient -
mieDirectionalG
directional scattering value
🌈 rainbow
First, create a shader with rainbow gradient effect Shader
, then use shader material ShaderMaterial
, create a circle THREE.TorusGeometry
and add it to the scene.
Vertex shader vertex.glsl :
varying vec2 vUV;
varying vec3 vNormal;
void main () {
vUV = uv;
vNormal = vec3(normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
Fragment shader fragment.glsl :
varying vec2 vUV;
varying vec3 vNormal;
void main () {
vec4 c = vec4(abs(vNormal) + vec3(vUV, 0.0), 0.1); // 设置透明度为0.1
gl_FragColor = c;
}
Rainbow gradient shader effect :
const material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
transparent: true,
uniforms: {},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
const geometry = new THREE.TorusGeometry(200, 10, 50, 100);
const torus = new THREE.Mesh(geometry, material);
torus.opacity = .1;
torus.position.set(0, -50, -400);
scene.add(torus);
💡
Shader shader
WebGL
The mechanism of coordinate transformation is described in the shader Shader
, the shader has the processing geometry vertex 顶点着色器
and the pixel processing 片段着色器
Two types
Prepare vertex shaders and fragment shaders
There are several ways to add shaders, the easiest way is to record the shader in HTML
. This method is implemented using the HTML
script
tag of ---a64b24ee3d8258cd408671d7cab66a83---, such as:
Vertex Shader :
<script id="vshader" type="x-shader/x-vertex"></script>
Fragment shader :
<script id="fshader" type="x-shader/x-fragment"></script>
🎏
It can also be created directly using theglsl
format file as in this article.
The three variables of the shader and how they work
-
Uniforms
: is a variable that has the same value for all vertices. For example light, fog, and shadow maps are data stored inuniforms
.uniforms
Can be accessed through vertex shaders and fragment shaders. -
Attributes
: is a variable associated with each vertex. For example, vertex positions, normals and vertex colors are all data stored inattributes
.attributes
can only be accessed in vertex shaders. -
Varyings
: is the variable passed from the vertex shader to the fragment shader. For each fragment, the value of eachvarying
will be a smooth interpolation of the adjacent vertex values.
顶点着色器
runs first, it receives attributes
, calculates the position of each individual vertex, and passes other data varyings
to the fragment shader. 片段着色器
After running, it sets the color of each individual fragment rendered to the screen.
💡
Three.js
the so-called material object Material
is essentially the shader code and needs to be passed uniform
data light source, color, matrix . Three.js
materials that provide direct rendering shader syntax ShaderMaterial
and RawShaderMaterial
.
-
RawShaderMaterial
: Same as the nativeWebGL
, there is basically no difference between the vertex shader and the fragment shader code, but the vertex data anduniform
Three.js
can be passed TheAPI
Three.js
-c5ab0e41d0675378a3aca3b32b2c5053--- is passed quickly, much more convenient than using theWebGL
nativeAPI
binding to the shader variable. -
ShaderMaterial
:ShaderMaterial
比RawShaderMaterial
方便些,着色器中的很多变量不用声明,Three.js
系统会自动设置,比如顶点Coordinate variables, projection matrices, view matrices, etc.
Constructor :
ShaderMaterial(parameters : Object)
parameters
: optional, an object that defines the appearance of the material, with one or more properties.
Common properties :
-
attributes[Object]
: accepts an object of the form,{ attribute1: { value: []} }
specifies theattributes
attribute
modified variable to be passed to the vertex shader code; The name and value are also in object format, such as{ value: [] }
,value
are fixed names, becauseattribute
is relative to all vertices, so an array format should be returned. OnlybufferGeometry
types can use this attribute. -
.uniforms[Object]
:如下形式的对象:{ uniform1: { value: 1.0 }, uniform2: { value: 2.0 }}
要传递给shader
代码的uniforms
;键uniform
, and the value is of the form:{ value: 1.0 }
wherevalue
is the value ofuniform
. The name must match the ---7610a01e421a88d3edec13b70d6d14d1--- in the shader codeuniform
name
defined in theGLSL
code. Note thatuniforms
is flushed frame by frame, so updating theuniform
value will immediately update theGLSL
corresponding value in the code. -
.fragmentShader[String]
:GLSL
code for the fragment shader, it can also be passed directly as a string or loaded withAJAX
. -
.vertexShader[String]
: TheGLSL
code of the vertex shader, which can also be passed directly as a string or loaded withAJAX
.
🌴 island
Next, use GLTFLoader
to load the island model and add it to the scene. You can use LoadingManager
to manage the loading progress before loading.
const manager = new THREE.LoadingManager();
manager.onProgress = async(url, loaded, total) => {
if (Math.floor(loaded / total * 100) === 100) {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
Animations.animateCamera(camera, controls, { x: 0, y: 40, z: 140 }, { x: 0, y: 0, z: 0 }, 4000, () => {
this.setState({ sceneReady: true });
});
} else {
this.setState({ loadingProcess: Math.floor(loaded / total * 100) });
}
};
const loader = new GLTFLoader(manager);
loader.load(islandModel, mesh => {
mesh.scene.traverse(child => {
if (child.isMesh) {
child.material.metalness = .4;
child.material.roughness = .6;
}
})
mesh.scene.position.set(0, -2, 0);
mesh.scene.scale.set(33, 33, 33);
scene.add(mesh.scene);
});
🦅 bird
Use GLTFLoader
to load the island model and add it to the scene, get the animation frame that comes with the model and play it. Remember to update the animation in requestAnimationFrame
. You can use the clone
method to add multiple birds to the scene. The bird model comes from the official website of Three.js
.
loader.load(flamingoModel, gltf => {
const mesh = gltf.scene.children[0];
mesh.scale.set(.35, .35, .35);
mesh.position.set(-100, 80, -300);
mesh.rotation.y = - 1;
mesh.castShadow = true;
scene.add(mesh);
const mixer = new THREE.AnimationMixer(mesh);
mixer.clipAction(gltf.animations[0]).setDuration(1.2).play();
this.mixers.push(mixer);
});
🖐 Interaction Points
Add an interaction point, the mouse hover
will display a prompt when hovering, click the interaction point to switch the camera angle, and focus on the position corresponding to the interaction point 📍
.
const points = [
{
position: new THREE.Vector3(10, 46, 0),
element: document.querySelector('.point-0')
},
// ...
];
document.querySelectorAll('.point').forEach(item => {
item.addEventListener('click', event => {
let className = event.target.classList[event.target.classList.length - 1];
switch(className) {
case 'label-0':
Animations.animateCamera(camera, controls, { x: -15, y: 80, z: 60 }, { x: 0, y: 0, z: 0 }, 1600, () => {});
break;
// ...
}
}, false);
});
🎥 Animation
Updated requestAnimationFrame
for water, lens track controller, camera, TWEEN
, interaction points, etc. in ---1269965a47620e1ab5af06a07dc349e8---.
const animate = () => {
requestAnimationFrame(animate);
water.material.uniforms['time'].value += 1.0 / 60.0;
controls && controls.update();
const delta = clock.getDelta();
this.mixers && this.mixers.forEach(item => {
item.update(delta);
});
const timer = Date.now() * 0.0005;
TWEEN && TWEEN.update();
camera && (camera.position.y += Math.sin(timer) * .05);
if (this.state.sceneReady) {
// 遍历每个点
for (const point of points) {
// 获取2D屏幕位置
const screenPosition = point.position.clone();
screenPosition.project(camera);
raycaster.setFromCamera(screenPosition, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length === 0) {
// 未找到相交点,显示
point.element.classList.add('visible');
} else {
// 找到相交点
// 获取相交点的距离和点的距离
const intersectionDistance = intersects[0].distance;
const pointDistance = point.position.distanceTo(camera.position);
// 相交点距离比点距离近,隐藏;相交点距离比点距离远,显示
intersectionDistance < pointDistance ? point.element.classList.remove('visible') : point.element.classList.add('visible');
}
const translateX = screenPosition.x * sizes.width * 0.5;
const translateY = - screenPosition.y * sizes.height * 0.5;
point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
}
}
renderer.render(scene, camera);
}
animate();
}
💡
Raycaster detects occlusion
Carefully observe, in the above 👆
updating the interaction point animation method, check whether the interaction point is occluded by the object through the raycaster
ray, if it is occluded, hide the interaction point, otherwise display the interaction point , you can observe this effect by rotating the scene.
💥 Lens flare
Add lens flare to the point light source Lensflare
effect, it looks more real and creates a full atmosphere!
import { Lensflare, LensflareElement } from 'three/examples/jsm/objects/Lensflare.js';
import lensflareTexture0 from '@/containers/Ocean/images/lensflare0.png';
import lensflareTexture1 from '@/containers/Ocean/images/lensflare1.png';
// 太阳点光源
const pointLight = new THREE.PointLight(0xffffff, 1.2, 2000);
pointLight.color.setHSL(.995, .5, .9);
pointLight.position.set(0, 45, -2000);
const textureLoader = new THREE.TextureLoader();
const textureFlare0 = textureLoader.load(lensflareTexture0);
const textureFlare1 = textureLoader.load(lensflareTexture1);
// 镜头光晕
const lensflare = new Lensflare();
lensflare.addElement(new LensflareElement(textureFlare0, 600, 0, pointLight.color));
lensflare.addElement(new LensflareElement(textureFlare1, 60, .6));
lensflare.addElement(new LensflareElement(textureFlare1, 70, .7));
lensflare.addElement(new LensflareElement(textureFlare1, 120, .9));
lensflare.addElement(new LensflareElement(textureFlare1, 70, 1));
pointLight.add(lensflare);
scene.add(pointLight);
Summarize
The new knowledge points included in this article mainly include:
-
Tone Mapping
-
Water
class -
Sky
class -
Shader
shader -
ShaderMaterial
shader material -
Raycaster
Detect occlusion -
Lensflare
lens flare
If you want to know other front-end knowledge or other knowledge not described in detail in this article Web 3D
development technology related knowledge, you can read my previous articles. Please indicate the original address and author when reprinting . If you think the article is helpful to you, don't forget to click three links 👍 .
refer to
- [1]. https://threejs.org
appendix
- My 3D column
- [1]. 🦊 Three.js implements a 3D open world game: Ali's multiverse
- [2]. 🔥 Three.js flame effect to realize the dynamic logo of Aerdun's Ring
- [3]. 🐼 Three.js Realize the 2022 Winter Olympics theme 3D fun page, including Bingdundun
-
...
- [1]. 📷 The front-end implements a very impressive browser-side scanning function
- [2]. 🌏 The Legend of Zelda: Breath of the Wild
- [3]. 😱 Realize cyberpunk 2077 style visual effects with CSS only a few steps
-
...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。