0
点赞
收藏
分享

微信扫一扫

Three.js学习项目--3D抗美援朝数据可视化

文章目录

部分场景

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

体验地址

https://kmyc.hongbin.xyz/

操作说明 视频

操作说明

我做了哪些(功能)
  • draco解析glb模型 同时处理部分纹理请求 减轻一次加载纹理压力
  • 手动控制轨道控制器镜头动画
  • 多音频拼接 控制
  • 封装动画播放器 控制进度切换
  • 动画进度控制器 同步音频 模拟视频体验
  • useContext状态共享
  • 自定义多级右键菜单 模拟原生菜单体验
  • 空闲时间加载后续用到的模型
  • 模型纹理&位置动态切换
  • echart图表使用
  • 浏览器自适应单位vw vmax使用(大面积使用)
  • 兼容移动端手机浏览
  • 模型的销毁和动画加载
  • useRef暴露方法多方调用(大量使用)
  • css-in-js 方案实践 css引擎styled-component
  • 未完成请求避免产生影响
  • 未执行计数器清理 字幕播放中 镜头切换动画执行中切换 后续的功能等
  • 点击不同模型产生不同效果 点击的事件监听 鼠标hover的样式
局限
  • 性能拉垮 考虑到诸多原因 未采用按需渲染在低配机上帧率大概只有30帧左右
  • 模型做的不精细-第一次建模
  • 在手机或者高刷设备上动画模型播放渲染速度与60帧设备不一致
源代码地址

https://gitee.com/honbingitee/kmyc

部分逻辑
按需渲染
controls.addEventListener('change', () => {
	renderer.render(scene, camera);
});
模型加载

https://hongbin.blog.csdn.net/article/details/122594047

动画控制器

https://hongbin.blog.csdn.net/article/details/123662686

模型纹理条件切换

/**
 * @description: 加载相册模型 返回相关操作回调
 * @param {number} animationIndex 战役索引
 * @param {THREE.TextureLoader} textureLoader 纹理加载器
 * @return {XCBack} XCBack
 */
export async function loadXCModel(
  animationIndex: number,
  textureLoader: THREE.TextureLoader
): Promise<XCBack> {
  const gltf = await window.gltfLoader.loadAsync(xcModel);
  const [model] = gltf.scene.children;
  const multiple = 5;
  model.position.y = 1.8 * multiple;

  if (animationIndex === 1) {
    model.rotateY(-Math.PI / 2);
    model.rotateX(-Math.PI / 10);
    model.rotateZ(Math.PI / 10);
  }

  const setZero = (mash: typeof model) => {
    mash.scale.x = 0;
    mash.scale.y = 0;
    mash.scale.z = 0;
  };
  setZero(model);
  /**
   * 设置不同的纹理 -- 切换图片
   */
  const setMaterial = (mash: Object3D, url: string) => {
    const texture = textureLoader.load(url);
    texture.flipY = false;
    texture.encoding = 3001;
    //@ts-ignore
    const mater = mash.material.clone(); //不能共用一个material 以为 instance.material 指向的都是同一个对象

    mater.map = texture;
    //@ts-ignore
    mash.material = mater;
  };

  const pictures: XCBack["models"] = [];

  for (let i = 0; i < 4; i++) {
    const instance = model.clone();
    //hover tip
    instance.userData.type = ModelType["Picture"];
    instance.userData.desc = XCDesc[animationIndex]
      ? XCDesc[animationIndex][i]
      : "";

    setMaterial(
      instance,
      `${process.env.REACT_APP_URL}xc/${animationIndex}-${i}-y.jpg`
    );
    if (animationIndex === 1) {
      instance.position.x = (i - 1) * -5 * multiple;
      instance.position.z = -14 * multiple;
    } else if (animationIndex === 2) {
      instance.position.x = -10 * multiple;
      instance.position.z = (i - 2) * 5 * multiple;
    } else {
      instance.position.x = -10 * multiple;
      instance.position.z = (i - 1) * 5 * multiple;
    }
    pictures.push(instance);
  }

  let timer1: number;
  let count = 0;
  const range = 30;
  const show = () => {
    if (count < range) {
      pictures.forEach(item => {
        item.scale.y += multiple / range;
        item.scale.z += (multiple / range) * 1.8;
        item.scale.x += multiple / range / 10;
      });
      count++;
      timer1 = requestAnimationFrame(show);
    }
  };

  const hide = () => {
    pictures.forEach(setZero);
    count = 0;
    cancelAnimationFrame(timer1);
    requestAnimationFrame(() => {
      cancelAnimationFrame(timer1);
    });
  };

  let prevIndex = animationIndex;

  const toggle: XCBack["toggle"] = nextIndex => {
    pictures.forEach((item, index) => {
      /**
       * hover 显示图片介绍
       */
      item.userData.type = ModelType["Picture"];
      item.userData.desc = XCDesc[nextIndex][index];

      setMaterial(
        item,
        `${process.env.REACT_APP_URL}xc/${nextIndex}-${index}-y.jpg`
      );
      //旋转角度
      if (nextIndex === 1) {
        if (prevIndex !== 1) {
          item.rotateY(-Math.PI / 2);
          item.rotateX(-Math.PI / 10);
          item.rotateZ(Math.PI / 10);
        }
      } else {
        if (prevIndex === 1) {
          item.rotateY(Math.PI / 2);
          item.rotateX(Math.PI / 10);
          item.rotateZ(Math.PI / 10);
        }
      }
      //位置
      if (nextIndex === 1) {
        item.position.x = (index - 1) * -5 * multiple;
        item.position.z = -14 * multiple;
      } else if (nextIndex === 2) {
        item.position.x = -10 * multiple;
        item.position.z = (index - 2) * 5 * multiple;
      } else {
        item.position.x = -10 * multiple;
        item.position.z = (index - 1) * 5 * multiple;
      }
    });
    prevIndex = nextIndex;
  };

  return {
    show,
    hide,
    models: pictures,
    toggle,
  };
}
模型加载同时请求部分纹理 生成进度条
//加载10个纹理
const loadTexture = () => {
    const textureLoader = new TextureLoader();

    for (let i = 0; i < 10; i++) {
      const index = i.toString().padStart(2, "0");
      const url = `${process.env.REACT_APP_URL}q/${i}.jpg`;
      const texture = textureLoader.load(
        url,
        _ => {
          setProgress(timeCheck(5));
        },
        undefined,
        err => {
          console.error("load texture fail:", err);
          setProgress(timeCheck(5));
        }
      );
      texture.flipY = false;
      texture.encoding = sRGBEncoding;
      addTexture(index, texture);
    }
  };
  
//draco解析模型
const dracoLoader = () => {
    let prevModel = 0;
    const manager = new LoadingManager();
    manager.onProgress = (_, loaded, total) => {
      const progress = Math.floor((loaded / total) * 100);
      if (progress === 100) return setProgress(timeCheck(50 - prevModel));
      prevModel += progress / 4;
      setProgress(timeCheck(progress / 4));
    };
    //设置错误信息
    manager.onError = setIsLoadFail;
    //创建draco解析器
    const dracoLoader = new DRACOLoader(manager);
    dracoLoader.setDecoderConfig({ type: "js" });
    dracoLoader.setDecoderPath(process.env.REACT_APP_URL as string);
    // gltf 加载器
    const gltfLoader = new GLTFLoader(manager);
    gltfLoader.setDRACOLoader(dracoLoader);
    gltfLoader.load(mapModel, setMap);
    //不带LoadingManager的加载器 如果使用gltfLoader会触发事件改变progress状态造成内存泄漏
    const normalGltfLoader = new GLTFLoader();
    normalGltfLoader.setDRACOLoader(dracoLoader);
    window.gltfLoader = normalGltfLoader;
  };
  
/**
 * 进度增长检测
  * @param {number} increase 增长的数值
  * @param {number} prev state原本数值
  * @return {number} newValue
  */
 const timeCheck = (increase: number) => (prev: number) => {
   if (increase + prev < 100) return prev + increase;
   // >= 100 检测时间
   if (Date.now() - enterTime > maxLoadTime) return 100;
   //time < maxLoadTime
   timer = setTimeout(() => {
     setProgress(100);
   }, maxLoadTime - (Date.now() - enterTime));
   //显示跳过按钮
   setIsCanJump(true);
   return prev;
 };
模型缩放小动画
let timer: number;
let i = 0;
const r = () => {
 if (i < 15) {
   timer = requestAnimationFrame(r);
 }
 for (const item of iconScene.children) {
   item.scale.x += 0.03;
   item.scale.y += 0.03;
   item.scale.z += 0.03;
 }
 i++;
};
r();
举报

相关推荐

0 条评论