likes
comments
collection
share

threejs 下雨场景

作者站长头像
站长
· 阅读数 61

介绍

该实例是threejs制作的下雨场景,使用Points创建一个粒子集合,PointsMaterial加贴图方式美化粒子,项目是一个练习demo,若有错误请指正,感谢大佬。

演示地址(在github上有点卡,请见谅)

实现

场景创建

let scene: THREE.Scene = new THREE.Scene();

相机

透视相机:PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

  • fov — 摄像机视锥体垂直视野角度
  • aspect — 摄像机视锥体长宽比
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面

若想了解正交相机与透视相机的区别请移步到官网demo

const initCamera = (width: number, height: number): void => {
  camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 40);
  // 设置相机位置
  camera.position.set(0, 0, 40);
  // 将相机添加到场景中
  scene.add(camera);
};

坐标辅助器

const initAxesHelper = (): void => {
  const axesHelper: THREE.AxesHelper = new THREE.AxesHelper(50);
  scene.add(axesHelper);
};

状态检测器

const initStats = (): void => {
  stats = new Stats();
  canvas.value.appendChild(stats.dom);
};

渲染器

这里说一下,有些同学在网上看的教程会有使用 WebGL1Renderer 来创建渲染器,那到底是使用WebGLRenderer还是WebGL1Renderer,接下来我们来了解下这两的区别

如果看过WebGL的同学应该知道WebGL有1和2两个版本,从r118起,WebGLRenderer会自动使用WebGL2来做渲染

那么这两个版本有何区别,简单说就是多了更多纹理格式、内置函数、3D 纹理贴图,同时还支持了非2的整数次方大小的图片。同时,WebGL2WebGL1 在对浏览器的兼容性上有很大的差异,这两者对浏览器兼容产生的巨大差异会导致陈旧的WebGL1的系统崩溃,所以threejs给我们提供了WebGL1Renderer来进行适配兼容

const initRenderer = (width: number, height: number): void => {
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  canvas.value.appendChild(renderer.domElement);
  renderer.render(scene, camera);
};

轨道控制器

const initControls = (): void => {
  controls = new OrbitControls(camera, renderer.domElement);
  // 使动画循环使用时阻尼或自转 意思是否有惯性
  controls.enableDamping = true;
  //是否可以缩放
  controls.enableZoom = true;
  //是否自动旋转
  controls.autoRotate = false;
  //是否开启右键拖拽
  controls.enablePan = true;
};

创建雨水粒子

const createRaining = (
  size: number,
  transparent: boolean,
  opacity: number,
  vertexColors: boolean,
  sizeAttenuation: boolean,
  color: number,
): void => {
  let geometry: THREE.BufferGeometry = new THREE.BufferGeometry();
  
  // pointCount * 3 共pointCount个点,每个点有x,y,z三个坐标,所以需要*3
  let positions: Float32Array = new Float32Array(pointCount * 3);
  for (let i = 0; i < pointCount * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 100;
  }
  // 设置顶点
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  
  // 加载雨滴贴图
  let textureLoader: THREE.TextureLoader = new THREE.TextureLoader();
  let rainTexture: THREE.Texture = textureLoader.load(rainingPng);
  // 点材质
  let material: THREE.PointsMaterial = new THREE.PointsMaterial({
    size: size, // 大小
    transparent: transparent, // 材质是否透明,配合opacity设置透明度
    opacity: opacity, // 透明度
    vertexColors: vertexColors, // 顶点着色
    sizeAttenuation: sizeAttenuation, // 指定点的大小是否因相机深度而衰减
    color: color, // 颜色
    depthTest: true, // 渲染此材质时启用深度测试
    depthWrite: false, // 渲染此材质是否对深度缓冲区有任何影响
    map: rainTexture, // 贴图
    alphaMap: rainTexture, // 贴图灰度
    blending: THREE.AdditiveBlending, // 材质混合模式
  });
  
  // 生成点
  point = new THREE.Points(geometry, material);
  // 添加到场景
  scene.add(point);
};

调试器

创建调试器控制粒子数据

const initGui = (): void => {
  const datGui = new dat.GUI();
  const guiConfig = {
    size: 1,
    transparent: true,
    opacity: 0.5,
    vertexColors: false,
    sizeAttenuation: true,
    color: 0xededed,
    rotateSystem: false,
    reDraw: () => {
      // 点存在时移除掉,避免创建相同的point导致性能变差
      if (point) {
        scene.remove(point);
      }
      createRaining(
        guiConfig.size,
        guiConfig.transparent,
        guiConfig.opacity,
        guiConfig.vertexColors,
        guiConfig.sizeAttenuation,
        guiConfig.color,
      );
      controls.autoRotate = guiConfig.rotateSystem;
    },
  };
  datGui.add(guiConfig, "size", 0.1, 3).onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "transparent").onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "opacity", 0.1, 1).onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "vertexColors").onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "sizeAttenuation").onChange(guiConfig.reDraw);
  datGui.addColor(guiConfig, "color").onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "rotateSystem").onChange(guiConfig.reDraw);

  guiConfig.reDraw();
};

渲染

const render = (): void => {
  controls.update();
  renderer.render(scene, camera);
  if (stats) {
    stats.update();
  }
  if (point) {
    // 雨水动画,修改顶点位置来模拟雨水降落效果
    const positions = (point.geometry.getAttribute("position") as any).array;
    for (let i = 0; i < pointCount * 3; i += 3) {
      positions[i + 1] -= Math.random() * 1;
      if (positions[i + 1] < -40) {
        positions[i + 1] = 40;
      }
    }
    // 更新顶点
    point.geometry.getAttribute("position").needsUpdate = true;
  }
  // 使用requestAnimationFrame函数进行帧渲染
  requestAnimationFrame(render);
};

完整代码 raining.vue

<template>
  <div id="canvas" ref="canvas"></div>
</template>

<script lang="ts" setup>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import dat from "dat.gui";
import Stats from "stats.js";
import { nextTick, ref } from "vue";
import rainingPng from "../assets/rain.png";

const canvas = ref<any>(null);
let scene: THREE.Scene = new THREE.Scene();
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let controls: any;
let stats: any;
let point: THREE.Points;
const pointCount: number = 5000;

nextTick(() => {
  initCamera(canvas.value.clientWidth, canvas.value.clientHeight);
  initRenderer(canvas.value.clientWidth, canvas.value.clientHeight);
  // initAxesHelper();
  initControls();
  render();
  initStats();
  initGui();
});

const initCamera = (width: number, height: number): void => {
  camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 40);
  camera.position.set(0, 0, 40);
  scene.add(camera);
};

const initRenderer = (width: number, height: number): void => {
  renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height);
  canvas.value.appendChild(renderer.domElement);
  renderer.render(scene, camera);
};

const initStats = (): void => {
  stats = new Stats();
  canvas.value.appendChild(stats.dom);
};

const initAxesHelper = (): void => {
  const axesHelper: THREE.AxesHelper = new THREE.AxesHelper(50);
  scene.add(axesHelper);
};

const initControls = (): void => {
  controls = new OrbitControls(camera, renderer.domElement);
  // 使动画循环使用时阻尼或自转 意思是否有惯性
  controls.enableDamping = true;
  //是否可以缩放
  controls.enableZoom = true;
  //是否自动旋转
  controls.autoRotate = false;
  //是否开启右键拖拽
  controls.enablePan = true;
};

const render = (): void => {
  controls.update();
  renderer.render(scene, camera);
  if (stats) {
    stats.update();
  }
  if (point) {
    const positions = (point.geometry.getAttribute("position") as any).array;
    for (let i = 0; i < pointCount * 3; i += 3) {
      positions[i + 1] -= Math.random() * 1;
      if (positions[i + 1] < -40) {
        positions[i + 1] = 40;
      }
    }
    point.geometry.getAttribute("position").needsUpdate = true;
  }
  requestAnimationFrame(render);
};

const initGui = (): void => {
  const datGui = new dat.GUI();
  const guiConfig = {
    size: 1,
    transparent: true,
    opacity: 0.5,
    vertexColors: false,
    sizeAttenuation: true,
    color: 0xededed,
    rotateSystem: false,
    reDraw: () => {
      if (point) {
        scene.remove(point);
      }
      createRaining(
        guiConfig.size,
        guiConfig.transparent,
        guiConfig.opacity,
        guiConfig.vertexColors,
        guiConfig.sizeAttenuation,
        guiConfig.color,
      );
      controls.autoRotate = guiConfig.rotateSystem;
    },
  };
  datGui.add(guiConfig, "size", 0.1, 3).onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "transparent").onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "opacity", 0.1, 1).onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "vertexColors").onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "sizeAttenuation").onChange(guiConfig.reDraw);
  datGui.addColor(guiConfig, "color").onChange(guiConfig.reDraw);
  datGui.add(guiConfig, "rotateSystem").onChange(guiConfig.reDraw);

  guiConfig.reDraw();
};

const createRaining = (
  size: number,
  transparent: boolean,
  opacity: number,
  vertexColors: boolean,
  sizeAttenuation: boolean,
  color: number,
): void => {
  let geometry: THREE.BufferGeometry = new THREE.BufferGeometry();

  let positions: Float32Array = new Float32Array(pointCount * 3);
  for (let i = 0; i < pointCount * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 100;
  }
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));

  let textureLoader: THREE.TextureLoader = new THREE.TextureLoader();
  let rainTexture: THREE.Texture = textureLoader.load(rainingPng);
  let material: THREE.PointsMaterial = new THREE.PointsMaterial({
    size: size,
    transparent: transparent,
    opacity: opacity,
    vertexColors: vertexColors,
    sizeAttenuation: sizeAttenuation,
    color: color,
    depthTest: true,
    depthWrite: false,
    map: rainTexture,
    alphaMap: rainTexture,
    blending: THREE.AdditiveBlending,
  });

  point = new THREE.Points(geometry, material);
  scene.add(point);
};

// 监听画面变化更新渲染画面
window.addEventListener("resize", () => {
  // 更新摄像机
  camera.aspect = canvas.value.clientWidth / canvas.value.clientHeight;
  // 更新摄像机投影矩阵
  camera.updateProjectionMatrix();
  // 更新渲染器
  renderer.setSize(canvas.value.clientWidth, canvas.value.clientHeight);
  // 设置渲染器的像素比
  renderer.setPixelRatio(window.devicePixelRatio);
});
</script>

转载自:https://juejin.cn/post/7197414501006999610
评论
请登录