本章节分为3点:
理解three.js 的渲染结构对canvas 进行响应式布局让canvas 画布自适应设备分辨率canvas 画布的css尺寸和像素尺寸 1.three.js 的渲染结构接下来我们对这个渲染过程做一个详细解释。
Three.js 封装了场景、灯光、阴影、材质、纹理和三维算法,让你不必再直接用WebGL 开发项目。
当然,我刚才说的是“不必再直接用WebGL”,有的时候我们会间接用到WebGL,比如自定义着色器。
three.js 在渲染三维场景时,需要创建很多对象,并将它们关联在一起。
下图便是一个基本的three.js 渲染结构。
解释一下上面的示意图:
Render是threejs的主要对象。当你讲一个场景Scene和一个摄像机Camera传递到渲染器的渲染方法中,渲染器便会将摄像机视椎体中三维场景渲染成一个二维图像显示在canvas画布中。
Scene场景对象场景对象是树状结构的,其中包含了三维对象Object3D和灯光对象Light。
Object3D可以看作是直接被渲染出来的,Object3D是网格对象Mesh和集合对象Group的基类。
场景对象可以定义场景的背景色和雾效。
在场景对象的树状结构中,每个对象的变换信息都是相对的。
比如汽车和汽车里的人,人的位置是相对于汽车而言的,当汽车移动了,人的本地坐标位坐标位虽然不变,但其世界坐标位已经变了。
Camera相机按理说相机对象是咋在场景里面的,但是相机对象不在它所看的场景里面,这就像我们自己看不见自己的眼睛一样。
因此相机对象可以独立于场景之外。
相机对象可以看到其他三维对象的子对象,这样相机就会随其父对象同步变换。
Mesh网格对象网格对象由几何体(Geometry)和材质(Material)2部分组成,Geometry负责塑形状,Material负责着色。
Geometry和Material可以被多个Mesh网格对象复用。
比如要绘制两个一模一样的立方体,那只需要实例化两个Mesh 即可,Geometry 和Materia可以使用一套。
Geometry几何体对象几何体对象负责塑形状,存储了顶点相关的数据,比如:顶点点位,顶点索引,uv坐标等。
threejs中内置了许多基本几何体,我么也可以自定义几何体,或者从外部的模型文件里加载几何体。
Material材质对象材质对象负责着色,绘制几何体的表面属性,比如:漫反射,镜面反射,光泽度,凹凸等。
Texture纹理对象纹理对象就是一张图像。纹理图像的图像源可以是image图片,canvas画布,video视频等。
Light光源对象Light对象不像Object3D那样依托于顶点,它更多的是像Object3D里面的材质Material,作用于物体的样式。
Light对象可以理解为:在几何体添加了材质之后,再利用光效配合材质对几何体的样式进行二次加工。
2-示例-绘制多个立方体绘制一个立方体:
效果如下图:
代码:
RenderStructure.tsx
import React, { useRef, useEffect } from "react";
import { BoxGeometry, DirectionalLight, Mesh, MeshNormalMaterial, MeshPhongMaterial, PerspectiveCamera, Scene, WebGLRenderer } from "three";
const { innerWidth, innerHeight } = window;
const scene = new Scene();
const camera = new PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.z = 5;
const renderer = new WebGLRenderer();
renderer.setSize(innerWidth, innerHeight);
const geometry = new BoxGeometry();
const material = new MeshNormalMaterial();
const cube = new Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
const RenderStructure: React.FC = (): JSX.Element => {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const { current } = divRef;
if (current) {
current.innerHTML = "";
current.append(renderer.domElement);
// 执行渲染动画
animate();
}
}, []);
return <div ref={divRef}></div>;
};
export default RenderStructure;
在上面的代码中,我没有直接建立 ,而是在WebGLRenderer 对象的实例化方法里建立的,在其源码可以找到相关逻辑:
function WebGLRenderer( parameters = {} ) {
const _canvas = parameters.canvas !== undefined ? parameters.canvas : createCanvasElement()
……
this.domElement = _canvas;
……
}
通过WebGLRenderer 对象建立了canvas后,再在react的函数组件的useEffect hook 中,将canvas 添加到div 中。
const RenderStructure: React.FC = (): JSX.Element => {
const divRef = useRef(null);
useEffect(() => {
const { current } = divRef;
current && current.append(renderer.domElement);
animate();
}, []);
return ;
};
当前这个立方体的材质是MeshNormalMaterial,并不受光照影响。
8.如果想受光源的影响则给立方体换个MeshPhongMaterial 材质,再添加光源。
const geometry = new BoxGeometry();
const material = new MeshPhongMaterial({ color: 0x44aa88 });
const cube = new Mesh(geometry, material);
scene.add(cube);
// 光的颜色
const color = 0xffffff;
// 光照强度
const intensity = 1;
// 实例化平行光
const light = new DirectionalLight(color, intensity);
// 设置光源的位置(x,y,z)
light.position.set(-1, 2, 4);
//光添加到场景中。
scene.add(light);
渲染效果如下:
在场景中添加2个一摸一样的立方体,可以发现:几何体和材质可被多个Mesh 对象共享。
const geometry = new BoxGeometry();
const material = new MeshPhongMaterial({ color: 0x44aa88 });
// 创建多个立方体。[-2, 0, 2]为立方体的位置
const cubes = [-2, 0, 2].map((num) => makeInstance(num));
// 把立方体添加到场景中
scene.add(...cubes);
function makeInstance(x: number) {
const cube = new Mesh(geometry, material);
cube.position.x = x;
return cube;
}
function animate() {
requestAnimationFrame(animate);
cubes.forEach((cube) => {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
});
renderer.render(scene, camera);
}
当前的渲染结构如下所示:
渲染效果如下所示:
对canvas 进行响应式布局canvas画布有2种尺寸:
像素尺寸:即canvas画布在高度和宽度上有多少个像素,默认是300 * 150
css尺寸:用css给canvas设置的尺寸。即css 里的width和height
canvas 的响应式布局需要考虑其像素尺寸。如果只考虑css尺寸则会对画布造成拉伸的效果。
案例:canvas 画布自适应浏览器的窗口的尺寸:
先取消renderer 的尺寸设置。// 设置canvas的尺寸。
//renderer.setSize(innerWidth, innerHeight);
2.用css 设置canvas 画布及其父元素的尺寸,使其充满窗口。
src/view/ResponsiveDesignconst ResponsiveDesign: React.FC = (): JSX.Element => {
……
return ;
};
src/view/fullScreen.css
html {
height: 100%;
}
body {
margin: 0;
overflow: hidden;
height: 100%;
}
#root,.canvasWrapper,canvas{
width: 100%;
height: 100%;
}
效果如下:我们可以看到立方体的边界出现了锯齿,这就是位图被css拉伸后失真导致的,默认canvas画布的尺寸只有300*150
因此,我们需要canvas不啊呼的像素尺寸自适应窗口。
3.创建一个让canvas像素尺寸随css尺寸同步更新的方法:
// 让canvas 像素尺寸随css 尺寸同步更新的方法
resizeRendererToDisplaySize(renderer);
// 将渲染尺寸设置为其显示的尺寸,返回画布像素尺寸是否等于其显示(css)尺寸的布尔值
function resizeRendererToDisplaySize(renderer) {
// 像素尺寸:width, height
// css尺寸: clientWidth, clientHeight
const { width, height, clientWidth, clientHeight } = renderer.domElement;
const needResize = width !== clientWidth || height !== clientHeight;
// 如果
if (needResize) {
// 设置canvas画布的尺寸基于像素尺寸设置。
//false:不会基于clientWidth, clientHeight设置canvas的尺寸。会基于width, height设置canvas的尺寸。
// renderer.setSize:设置canvas的尺寸
renderer.setSize(clientWidth, clientHeight, false);
}
return needResize;
}
renderer.setSize(w,h,bool) 是重置渲染尺寸的方法,在此方法里会根据w,h参数重置canvas 画布的尺寸。
this.setSize = function ( width, height, updateStyle ) {
if ( xr.isPresenting ) {
console.warn( 'THREE.WebGLRenderer: Can\'t change size while VR device is presenting.' );
return;
}
_width = width;
_height = height;
_canvas.width = Math.floor( width * _pixelRatio );
_canvas.height = Math.floor( height * _pixelRatio );
if ( updateStyle !== false ) {
_canvas.style.width = width + 'px';
_canvas.style.height = height + 'px';
}
this.setViewport( 0, 0, width, height );
};
setSize() 方法中的bool 参数很重要,会用于判断是否设置canvas 画布的css 尺寸。
当canvas画布的尺寸变化了,相机适口的宽高比也需要同步调整。这样我们拖拽浏览器的边界,缩放浏览器的时候,就可以看到canvas 画布自适应浏览器的尺寸了。function animate() {
requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(renderer)) {
const { clientWidth, clientHeight } = renderer.domElement;
// camera.aspect:设置相机适口的宽高比例:
camera.aspect = clientWidth / clientHeight;
// 更新相机的投影矩阵
camera.updateProjectionMatrix();
}
cubes.forEach((cube) => {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
});
renderer.render(scene, camera);
}
camera.aspect 属性是相机视口的宽高比我们在WebGL 里说透视投影矩阵的时候说过,当相机视口的宽高比变了,相机的透视投影矩阵也会随之改变,因此我们需要使用camera.updateProjectionMatrix() 方法更新透视投影矩阵。至于我们为什么不把更新相机视口宽高比的方法一起放进resizeRendererToDisplaySize()里,这是为了降低resizeRendererToDisplaySize() 方法和相机的耦合度。具体要不要这么做视项目需求而定。
让canvas 画布自适应设备分辨率
不同设备的显示器的分辨率是不一样的。
以上图中的iPhone6/7/8 为例:
375*667 代表的手机的屏幕的物理尺寸,如果我们在其中建立一个100% 充满屏幕的,那其尺寸就是375*667。Dpr 代表像素密度,2 表示手机屏幕在宽度上有375*2 个像素,在高度上有667*2 个像素,因此iPhone6/7/8 的屏幕的像素尺寸就是750*1334。当我们在这种像素尺寸大于物理尺寸的高分辨率显示器里绘图的时候,就需要考虑一个问题。
若我们直接在iPhone6/7/8 里建立一个充满屏幕的canvas,那其像素尺寸就是375*667。
这个尺寸并没发挥高分辨率显示器的优势,我们需要先将其像素尺寸设置为750*1334,然后再将其css 尺寸设置为375*667。
这样,就可以让canvas画布以高分辨率的姿态显示在显示器里。
代码示例:
function resizeRendererToDisplaySize(renderer: WebGLRenderer) {
const { width, height, clientWidth, clientHeight } = renderer.domElement;
// 上面的devicePixelRatio 就是设备像素密度,是window下的属性,即window.devicePixelRatio。
const [w, h] = [clientWidth * window.devicePixelRatio, clientHeight * window.devicePixelRatio];
const needResize = width !== w || height !== h;
if (needResize) {
renderer.setSize(w, h, false);
}
return needResize;
}
上面的devicePixelRatio 就是设备像素密度,是window下的属性,即window.devicePixelRatio。
其实,有的时候若不刻意观察,canvas 有没有自适应设备分辨率是很难看出的。
因此,若是对画面的渲染质量要求不高,可以什么都不做,这样也能避免canvas 画布像素尺寸变大后降低渲染效率的问题。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)