13
头图
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 MappingWater类、 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

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补间动画方法的封装; waterTextureflamingoModelislandModelvertexShader 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 the glsl 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 in uniforms . 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 in attributes . 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 each varying 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 native WebGL , there is basically no difference between the vertex shader and the fragment shader code, but the vertex data and uniform Three.js can be passed The API Three.js -c5ab0e41d0675378a3aca3b32b2c5053--- is passed quickly, much more convenient than using the WebGL native API binding to the shader variable.
  • ShaderMaterialShaderMaterialRawShaderMaterial方便些,着色器中的很多变量不用声明, 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 the attributes 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, because attribute is relative to all vertices, so an array format should be returned. Only bufferGeometry 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 } where value is the value of uniform . The name must match the ---7610a01e421a88d3edec13b70d6d14d1--- in the shader code uniform name defined in the GLSL code. Note that uniforms is flushed frame by frame, so updating the uniform value will immediately update the GLSL corresponding value in the code.
  • .fragmentShader[String] : GLSL code for the fragment shader, it can also be passed directly as a string or loaded with AJAX .
  • .vertexShader[String] : The GLSL code of the vertex shader, which can also be passed directly as a string or loaded with AJAX .

🌴 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

appendix


dragonir
1.8k 声望3.9k 粉丝

Accepted ✔