基于ThreeJS框架React-Three-Fiber实现3D看房

1,001 阅读6分钟

React-three-fiber框架是一个基于ThreeJS二次封装的3D可视化框架,简称R3F,是ThreeJS的React渲染器。可用ThreeJS做的一切都可以用react-three-fiber来完成,同时支持Class版本和Hooks版本。并且R3F能有效降低开发难度和代码量。此外,与3D对象的事件交互也变得相对更容易。

在R3F中,提倡的是组件化开发,所有在ThreeJS中可以找到的元素,都可以使用JSX语法表示,比如用<mesh /> 表示new THREE.Mesh()。这使得我们可以在JSX中直接表达实例的创建。

基本概念介绍

场景(scene)

场景是一个三维空间,可以理解为一个无限大的容器,容器里面默认有一个你看不见的世界坐标原点及3个坐标轴。你可以往里面放置任意的物体、灯光、摄像机等。在ThreeJS中可以用Scene实例化一个场景对象。使用这个场景实例的add()方法可以向场景中添加3D对象。

import {Scene} from "three";

const scene = new Scene();

// 向场景中添加物体
scene.add(...);

在ThreeJS中,场景中的坐标系是右手坐标系。即是右手位于坐标原点,大拇指,食指,中指两两成直角,大拇指指向x轴正方向,食指指向y轴正方向时,则中指所指的方向就是z轴正方向。

axis.jpeg

相机(camera)

相机决定了场景中哪个角度的景色会显示出来。就像人的眼睛一样,站在不同的位置,能够看到不同的景色和物体。

值得一提的是,场景只有一种,但是相机可以有多种。与现实中一样,不同的相机擅长方面也不一样。有的适合人像,有的适合风景。专业摄影师根据实际用途不一样,选择使用不同的相机。对于程序员来说,只需要设置不同的相机参数,就能让相机产生对应的效果。常用的相机是透视相机和正交投影相机。

透视相机

透视相机是模拟人眼的视觉,近大远小。在ThreeJS中的创建方式如下:

import {PerspectiveCamera} from "three";

const camera = new PerspectiveCamera45, width / height, 11000 );

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number ) :

fov: 相机视锥体竖直方向视野角度

aspect: 相机视锥体水平方向和竖直方向长度比,一般设置为Canvas画布宽高比width / height

near: 相机视锥体近截面相对相机距离

far: 相机视锥体远截面相对相机距离

只有在离相机距离大于near,小于far,并且在相机的可视角度之内,才能被相机投影到。

正交投影相机

没有近大远小的效果,我们所熟知的工程制图中的三视图就是很典型的正交投影,类似于平:行光投影。在ThreeJS中的创建方式如下:

import {OrthographicCamera} from "three";

const camera = new OrthographicCamera( width / - 2, width / 2, height / 2, height / - 211000 );

OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number ):

left: 渲染空间的左边界

right: 渲染空间的右边界

top: 渲染空间的上边界

bottom: 渲染空间的下边界

near: 表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。默认值0.1

far: 表示的是距离相机多远的位置截止渲染,如果设置的值偏小,会有部分场景看不到。默认值2000

渲染器(renderer)

渲染器的工作是将场景、物体、摄像机等这些数据作为渲染参数进行渲染,决定最终页面上元素应该呈现出什么样的东西。ThreeJS中可以使用WebGLRenderer创建一个webgl渲染器。

import {WebGLRendererfrom "three";

let renderer = new WebGLRenderer();

renderer.setSize(window.innerWidth, window.innerHeight);  // 设置画布尺寸
renderer.setPixelRatio(window.devicePixelRatio);  // 设置设备像素

document.body.appendChild(renderer.domElement); // 把画布canvas元素插入html

R3F渲染3D看房

根据我个人实践和了解,目前常用于实现全景看房效果的方法有两种,分别为纹理贴图和3D建模。本文主要讲纹理贴图,纹理贴图全景看房又可以有天空盒贴图和全景图片贴图;

首先使用R3F创建场景,相机,轨道控制器:

<div id={styles.house}>
    <Canvas>
        {/*设置画布背景色*/}
      <color attach="background" args={["#dedede"]} />
      <PerspectiveCamera
        fov={6}
        aspect={window.innerWidth/window.innerHeight}
        near={0.1}
        far={10}
        position={[0, 0, 0]}
      />
      <OrbitControls
        minPolarAngle={Math.PI/2}
        maxPolarAngle={Math.PI/2}
        minDistance={0.1}
          maxDistance={5} />
      {/*环境光*/}
        <ambientLight intensity={1} args={["#dedede"]}  />
    </Canvas>
</div>

全景贴图实现

全景贴图实现是使用相机拍一张房屋的全景图片,然后把图片以纹理的形式添加到球体上,调整球体的渲染面side为BackSide,再配合z轴的适当缩放(正负都可以)即可。

{/*球体实现*/}
<mesh onClick={handleClick} scale={[2, 2, -2]} position={[0, 0, 0]}>
  <sphereGeometry ref={boxRef} args={[3]} />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={quanMap} attach={"material"} />
</mesh>

天空盒贴图实现

天空盒的原理是将我们所处的场景看成是由前后、左右、上下6个面组成的,将我们所看到的6个面的视觉镜像处理成图片,将它们分别以纹理的形式贴到立方体对应的面。这时,如果我们处于这个立方体中(适当的缩放),即可还原当时的场景。材料的attach属性表示的是当前材料贴附哪个面,所以请注意它们之间对应关系。

{/*正方体实现*/}
<mesh scale={[2, 2, 2]} position={[0, 0, 0]}>
  <boxGeometry ref={boxRef} args={[5, 5, 5]} />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={leftMap} attach={"material-1"} />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={rightMap} attach={"material-0"} />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={topMap} attach={"material-2"} />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={bottomMap} attach={"material-3"}  />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={frontMap} attach={"material-5"} />
  <meshStandardMaterial side={BackSide} toneMapped={false} map={backMap} attach={"material-4"} />
</mesh>

项目全部代码如下:

import styles from "./index.module.scss";
import {Canvas, useLoader} from "@react-three/fiber";
import {CameraControls, OrbitControls, PerspectiveCamera, Box, MeshTransmissionMaterial, MeshDiscardMaterial} from "@react-three/drei";
import {BackSide, BoxGeometry, DoubleSide, TextureLoader, FrontSide} from "three";

const HouseSky: React.FC = () => {
  const boxRef = useRef();
  // 相对位置需要使用import转换一次才能给它用
  // 绝对位置, 在public文件夹下
  const topMap = useLoader(TextureLoader, "/textures/houseImg/top.png"),
    bottomMap = useLoader(TextureLoader, "/textures/houseImg/bottom.png"),
    leftMap = useLoader(TextureLoader, "/textures/houseImg/left.png"),
    rightMap = useLoader(TextureLoader, "/textures/houseImg/right.png"),
    frontMap = useLoader(TextureLoader, "/textures/houseImg/front.png"),
    backMap = useLoader(TextureLoader, "/textures/houseImg/back.png"),
    quanMap = useLoader(TextureLoader, "/textures/houseImg/quan.png");


  // 放在Canvas里才能拿到boxRef
  const BoxConfig = () => {
    useEffect(() => {
      // console.log(boxRef.current);
    }, [boxRef.current]);

    return null;
  };

  // 点击
  const handleClick = (d) => {
    // console.log(d);
  };


  return(
    <div id={styles.house}>
      <Canvas>
        <color attach="background" args={["#dedede"]} />
        <PerspectiveCamera
          fov={6}
          aspect={window.innerWidth/window.innerHeight}
          near={0.1}
          far={10}
          position={[0, 0, 0]}
        />
        <OrbitControls
          minPolarAngle={Math.PI/2}
          maxPolarAngle={Math.PI/2}
          minDistance={0.1}
          maxDistance={5} />
        {/*<axesHelper args={[200]} />*/}
        {/*没添加光,随便设置设么颜色都看不见*/}
        {/*环境光*/}
        <ambientLight intensity={1} args={["#dedede"]}  />
        {/*平行光*/}
        {/*此处不需要平行光*/}
        {/*<directionalLight intensity={1} position={[0, 0, 200]} />*/}
        {/*材料渲染的side设为BackSide再配合z轴的缩放,即可在内部*/}
        {/*球体实现*/}
        {/*<mesh onClick={handleClick} scale={[2, 2, 2]} position={[0, 0, 0]}>
          <sphereGeometry ref={boxRef} args={[3]} />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={quanMap} attach={"material"} />
          <BoxConfig />
        </mesh>*/}
        {/*正方体实现*/}
        <mesh scale={[2, 2, 2]} position={[0, 0, 0]}>
          <boxGeometry ref={boxRef} args={[5, 5, 5]} />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={leftMap} attach={"material-1"} />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={rightMap} attach={"material-0"} />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={topMap} attach={"material-2"} />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={bottomMap} attach={"material-3"}  />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={frontMap} attach={"material-5"} />
          <meshStandardMaterial side={BackSide} toneMapped={false} map={backMap} attach={"material-4"} />
        </mesh>
      </Canvas>
    </div>
  );
};

export default HouseSky;

如需要下载相关图片,请访问二师兄github上当前demo的项目的图片存放位置:

github.com/hzzou/hzzou…

qrcode_for_gh_599d8540a289_258.jpg

扫码关注二师微信公众号

文章若有错误,恳请大家指出问题所在,本人不胜感激 。不懂的地方可以评论,我都会一一回复。文章对大家有帮助的话,希望大家能动手点赞鼓励。