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轴正方向。
相机(camera)
相机决定了场景中哪个角度的景色会显示出来。就像人的眼睛一样,站在不同的位置,能够看到不同的景色和物体。
值得一提的是,场景只有一种,但是相机可以有多种。与现实中一样,不同的相机擅长方面也不一样。有的适合人像,有的适合风景。专业摄影师根据实际用途不一样,选择使用不同的相机。对于程序员来说,只需要设置不同的相机参数,就能让相机产生对应的效果。常用的相机是透视相机和正交投影相机。
透视相机
透视相机是模拟人眼的视觉,近大远小。在ThreeJS中的创建方式如下:
import {PerspectiveCamera} from "three";
const camera = new PerspectiveCamera( 45, width / height, 1, 1000 );
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 / - 2, 1, 1000 );
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 {WebGLRenderer} from "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的项目的图片存放位置:
扫码关注二师微信公众号
文章若有错误,恳请大家指出问题所在,本人不胜感激 。不懂的地方可以评论,我都会一一回复。文章对大家有帮助的话,希望大家能动手点赞鼓励。