纹理
纹理总览
纹理(Texture), 是在 3D 渲染中最常用到的资源之一。我们在给模型着色时,需要给每个片元设置一个颜色值,这个色值除了直接手动设置,我们还可以选择从纹理中读取纹素进行着色,从而达到更加丰富的美术效果。
纹理类型
类型 | 描述 |
---|---|
2D纹理 | 最常用的美术资源,使用二维 UV 坐标进行采样 |
十字立方纹理 | 6张 2D 纹理组成了一个十字立方纹理,可以用来实现天空盒、环境反射等特效 |
LDR立方纹理 | 6张 LDR 纹理组成了一个全景天空图,可以用来实现天空盒、环境反射等特效 |
HDR纹理 | 支持 RGBE 格式的采样纹理 |
HDR立方纹理 | 6张 HDR 纹理组成了一个全景天空图,可以用来实现天空盒、环境反射等特效 |
创建贴图
1. 手动创建2D纹理
通过创建纹理实例,我们可以手动创建一个贴图对象,再通过 load
手动加载对应的图片资源:
2D纹理
支持 web 常见的图片格式,jpg/png/webp
;HDR纹理
支持加载RGBE
格式的.hdr
图片;
import { BitmapTexture2D } from '@orillusion/core';
// 创建2D纹理
let texture = new BitmapTexture2D();
// 加载贴图资源
texture.load('path/to/image.png');
// 创建 HDR 贴图
let hdrTexture = new HDRTexture();
hdrTexture = await hdrTexture.load('path/to/image.hdr');
2. 通过资源管理器加载
除了手动创建纹理对象,我们推荐通过 资源管理器 便捷的加载图片并自动创建对应的纹理贴图:
import { Engine3D } from '@orillusion/core';
// 2D纹理
let texture = Engine3D.res.loadTexture('path/to/image.png');
// HDR贴图
let hdrTexture = Engine3D.res.loadHDRTexture('path/to/image.hdr');
// 十字立方纹理
let texture = Engine3D.res.loadTextureCube('path/to/sky.png');
// LDR全景图
let HDRTextureCube = Engine3D.res.loadLDRTextureCube('path/to/sky.png');
// HDR全景图
let HDRTextureCube = Engine3D.res.loadHDRTextureCube('path/to/sky.hdr');
3. 手动填写颜色数据
纹理底层其实对应着每个像素的颜色值,即 RGBA
通道,我们可以手动创建 Uint8Array
填写 rgba
颜色通道的具体数值,然后通过 Uint8ArrayTexture 类手动创建纹理:
// 图片参数
let w = 32;
let h = 32;
let r = 255;
let g = 0;
let b = 0;
let a = 255;
// 创建 raw Uint8Array
let textureData = new Uint8Array(w * h * 4);
// 填充 rgba 数值
for (let i = 0; i < w; i++) {
for (let j = 0; j < h; j++) {
let pixelIndex = j * w + i;
textureData[pixelIndex * 4 + 0] = r;
textureData[pixelIndex * 4 + 1] = g;
textureData[pixelIndex * 4 + 2] = b;
textureData[pixelIndex * 4 + 3] = a;
}
}
// 通过 rawData 创建贴图
let texture = new Uint8ArrayTexture();
texture.create(16, 16, textureData, true);
加载贴图
2D贴图
我们可以直接将纹理赋予材质球的相应属性,比如基础的贴图 (baseMap)
:
let floorMat = new LitMaterial();
let texture = await Engine3D.res.loadTexture('path/to/image.png');
floorMat.baseMap = texture;
import { Engine3D, Vector3, Scene3D, Object3D, Camera3D, AtmosphericComponent, View3D, UnLitMaterial, MeshRenderer, HoverCameraController, PlaneGeometry, DirectLight, Color } from '@orillusion/core';
await Engine3D.init();
let scene = new Scene3D();
let camera = new Object3D();
scene.addChild(camera);
let mainCamera = camera.addComponent(Camera3D);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let hc = camera.addComponent(HoverCameraController);
hc.setCamera(0, 0, 2);
// create a unlit material
let mat = new UnLitMaterial();
let texture = await Engine3D.res.loadTexture('https://cdn.orillusion.com/gltfs/cube/material_02.png');
mat.baseMap = texture;
// add a plane to display the image
let planeObj = new Object3D();
let mr = planeObj.addComponent(MeshRenderer);
mr.geometry = new PlaneGeometry(2, 2, 10, 10, Vector3.Z_AXIS);
mr.material = mat;
scene.addChild(planeObj);
// add a light
let lightObj = new Object3D();
lightObj.rotationX = -45;
let light = lightObj.addComponent(DirectLight);
light.lightColor = new Color(1.0, 1.0, 1.0, 1.0);
light.intensity = 10;
scene.addChild(lightObj);
// add an Atmospheric sky enviroment
scene.addComponent(AtmosphericComponent).sunY = 0.6;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);
十字立方贴图
十字立方贴图
有 6 个面,即用 6 张 2D 纹理按下图的顺序排列组合成一个立方盒子:
十字立方纹理
可以用来实现天空盒、环境反射等特效,我们推荐直接使用 Res 方式加载 1
张完整的十字立方贴图,直接赋值给 scene.envMap
即可:
// 加载一张十字立方贴图
let textureCube = Engine3D.res.loadTextureCube('path/to/crossSky.png');
// 设置天空盒
scene.envMap = textureCube;
除此之外,我们也可以通过 BitmapTextureCube 类手动加载 6
个独立面的立方贴图:
let textureCube = new BitmapTextureCube();
// 分别加在独立6个面
await textureCube.load([
'x Right',
'-x Left',
'y Up',
'-y Down',
'z Front',
'-z Back'
]);
import { Camera3D, Engine3D, View3D, HoverCameraController, Object3D, Scene3D, BitmapTextureCube, SkyRenderer } from '@orillusion/core';
await Engine3D.init();
let scene = new Scene3D();
let camera = new Object3D();
scene.addChild(camera);
let mainCamera = camera.addComponent(Camera3D);
mainCamera.perspective(60, Engine3D.aspect, 1, 2000.0);
let ctrl = camera.addComponent(HoverCameraController);
ctrl.setCamera(180, 0, 10);
let evnMap = new BitmapTextureCube();
let urls: string[] = [];
urls.push('https://cdn.orillusion.com/textures/cubemap/skybox_nx.png');
urls.push('https://cdn.orillusion.com/textures/cubemap/skybox_px.png');
urls.push('https://cdn.orillusion.com/textures/cubemap/skybox_py.png');
urls.push('https://cdn.orillusion.com/textures/cubemap/skybox_ny.png');
urls.push('https://cdn.orillusion.com/textures/cubemap/skybox_nz.png');
urls.push('https://cdn.orillusion.com/textures/cubemap/skybox_pz.png');
await evnMap.load(urls);
let sky = scene.addComponent(SkyRenderer);
sky.map = evnMap;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);
全景立方贴图
除了 十字立方贴图
,我们也可以通过 Res 加载全景 (equirectangular) 类型的贴图。同时支持 RGBA
类型的普通图片和支持 RGBE
格式的 hdr
图片:
// 普通格式全景图
let ldrTextureCube = await Engine3D.res.loadLDRTextureCube('path/to/sky.png');
// 加载hdr全景贴图
let hdrTextureCube = await Engine3D.res.loadHDRTextureCube('path/to/sky.hdr');
import { Camera3D, Engine3D, View3D, HoverCameraController, Object3D, Scene3D, SkyRenderer } from '@orillusion/core';
await Engine3D.init();
let scene = new Scene3D();
let sky = scene.addComponent(SkyRenderer);
let hdrTextureCube = await Engine3D.res.loadHDRTextureCube('https://cdn.orillusion.com/hdri/T_Panorama05_HDRI.HDR');
sky.map = hdrTextureCube;
let camera = new Object3D();
scene.addChild(camera);
let mainCamera = camera.addComponent(Camera3D);
mainCamera.perspective(60, Engine3D.aspect, 1, 2000.0);
let ctrl = camera.addComponent(HoverCameraController);
ctrl.setCamera(180, 0, 10);
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
Engine3D.startRenderView(view);
纹理设置
1. 纹理重复
纹理采样的默认范围为[0,1]
, 即平铺纹理到整个平面,我们可以通过设置 材质 的 uvTransform_1 属性,手动的改变贴图重复的坐标范围:
let mat = new LitMaterial();
// 使得贴图在 水平 和 竖直 方向上重复2次
mat.uvTransform_1 = new Vector4(0,0,2,2);
mat.baseMap = new BitmapTexture2D();
当纹理 uvtransform_1
超出 [0,1]
范围时,我们可以通过设置纹理的 addressModeU
和 addressModeV
两个属性,来控制水平方向和竖直方向上重复的方式,例如:
let texture = new BitmapTexture2D();
// 水平方向,默认 repeat 模式
texture.addressModeU = GPUAddressMode.repeat;
// 竖直方向, 默认 repeat 模式
texture.addressModeV = GPUAddressMode.repeat;
目前 WebGPU
默认支持以下几种循环模式:
- 重复模式(repeat):默认模式,即对超出范围从
[0,1]
开始重新采样
- 镜像重复模式(mirror_repeat):对超出范围经过镜像翻转后在从
[0,1]
开始重新采样。
- 截取模式(clamp_to_edge):超出范围采样纹理边缘纹素颜色。
import { Engine3D, Vector3, Scene3D, Object3D, Camera3D, AtmosphericComponent, View3D, UnLitMaterial, MeshRenderer, HoverCameraController, PlaneGeometry, Vector4, GPUAddressMode, DirectLight, Color } from '@orillusion/core';
await Engine3D.init();
let scene = new Scene3D();
let camera = new Object3D();
scene.addChild(camera);
let mainCamera = camera.addComponent(Camera3D);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let hc = camera.addComponent(HoverCameraController);
hc.setCamera(0, 0, 2);
// add a dir light
let lightObj = new Object3D();
lightObj.rotationX = -45;
let light = lightObj.addComponent(DirectLight);
light.lightColor = new Color(1.0, 1.0, 1.0, 1.0);
light.intensity = 10;
scene.addChild(lightObj);
// add an Atmospheric sky enviroment
scene.addComponent(AtmosphericComponent).sunY = 0.6;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);
let texture = await Engine3D.res.loadTexture('https://cdn.orillusion.com/images/webgpu.webp');
// texture.addressModeU = GPUAddressMode.repeat;
// texture.addressModeV = GPUAddressMode.repeat;
let mat = new UnLitMaterial();
mat.setUniformVector4('transformUV1', new Vector4(0, 0, 2, 2));
mat.baseMap = texture;
let planeObj = new Object3D();
let mr = planeObj.addComponent(MeshRenderer);
mr.geometry = new PlaneGeometry(2, 2, 10, 10, Vector3.Z_AXIS);
mr.material = mat;
scene.addChild(planeObj);
let select = document.createElement('select');
select.innerHTML = `
<option value="repeat">Repeat</option>
<option value="mirror_repeat">Mirror-Repeat</option>
<option value="clamp_to_edge">Clamp-to-Edge</option>
`;
select.setAttribute('style', 'position:fixed;right:5px;top:5px');
document.body.appendChild(select);
select.addEventListener('change', () => {
texture.addressModeU = GPUAddressMode[select.value];
texture.addressModeV = GPUAddressMode[select.value];
});
2. 采样过滤模式
一般来说,纹素和屏幕像素不会刚好对应,这就需要 GPU
去缩放像素大小。但不同的缩放模式会对最终像素颜色有一定的影响。我们可以通过设置纹理的 magFilter
和 minFilter
属性来控制 GPU
放大(Mag)和 缩小(Min)像素时采用的过滤模式。
let texture = new BitmapTexture2D();
// 放大模式,默认 linear 模式
texture.magFilter = 'linear';
// 缩小模式,默认 linear 模式
texture.minFilter = 'linear';
目前 WebGPU
支持 linear
线性采样 和 nearest
临近点采样模式。
一般来说 linear
模式像素边缘更加平滑,适合复杂的图形过渡;nearest
像素边缘更加锐利,适合颜色分布清晰,边缘明显的纹理,可以通过如下例子,看到不同的采样模式对贴图变现的影响:
import { Engine3D, Vector3, Scene3D, Object3D, Camera3D, AtmosphericComponent, View3D, UnLitMaterial, MeshRenderer, HoverCameraController, PlaneGeometry, BitmapTexture2D, DirectLight, Color } from '@orillusion/core';
await Engine3D.init();
let scene = new Scene3D();
let camera = new Object3D();
scene.addChild(camera);
let mainCamera = camera.addComponent(Camera3D);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let hc = camera.addComponent(HoverCameraController);
hc.setCamera(0, 0, 0.2);
// add a dir light
let lightObj = new Object3D();
lightObj.rotationX = -45;
let light = lightObj.addComponent(DirectLight);
light.lightColor = new Color(1.0, 1.0, 1.0, 1.0);
light.intensity = 10;
scene.addChild(lightObj);
// add an Atmospheric sky enviroment
scene.addComponent(AtmosphericComponent).sunY = 0.6;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);
let texture = new BitmapTexture2D();
await texture.load('https://cdn.orillusion.com/gltfs/cube/material_02.png');
texture.magFilter = 'linear';
texture.minFilter = 'linear';
let mat = new UnLitMaterial();
mat.baseMap = texture;
let planeObj = new Object3D();
let mr = planeObj.addComponent(MeshRenderer);
mr.geometry = new PlaneGeometry(2, 2, 10, 10, Vector3.Z_AXIS);
mr.material = mat;
scene.addChild(planeObj);
let select = document.createElement('select');
select.innerHTML = `
<option value="linear">Linear</option>
<option value="nearest">Nearest</option>
`;
select.setAttribute('style', 'position:fixed;right:5px;top:5px');
document.body.appendChild(select);
select.addEventListener('change', () => {
texture.magFilter = select.value;
texture.minFilter = select.value;
});
3. 多级渐远纹理 (Mipmap)
在3D世界中,因为不同的物体和摄像机距离有近有远,对应的纹理图像有大有小。如果采用同样分辨率的纹理,对于远处的物体需要从高分辨率的原始图像中拾取一小分部像素颜色,这不仅浪费 GPU
性能,也会因为像素畸变造成不真实的感觉或产生大量 摩尔纹
。Orillusion
使用一种 多级渐远纹理 (Mipmap)
的概念来解决这个问题,简单说就是对一个高分辨的图形自动缩放成一系列不同分辨率的纹理。根据贴图和观察者的距离不同,使用不同分辨率的贴图。远距离的物体使用低分辨的纹理,解析度更自然,而且也能有效节省 GPU
性能。
我们可以通过 useMipmap
来进行开启或关闭,默认开启
let texture = new BitmapTexture2D();
// 默认为 true
texture.useMipmap = true;
import { Engine3D, Scene3D, Object3D, Camera3D, AtmosphericComponent, View3D, UnLitMaterial, MeshRenderer, PlaneGeometry, BitmapTexture2D, Vector4, OrbitController, DirectLight, Color } from '@orillusion/core';
await Engine3D.init();
let scene = new Scene3D();
let camera = new Object3D();
camera.y = 10;
camera.z = 30;
scene.addChild(camera);
let mainCamera = camera.addComponent(Camera3D);
mainCamera.perspective(60, Engine3D.aspect, 0.1, 10000.0);
let oribit = camera.addComponent(OrbitController);
oribit.autoRotate = true;
// add a dir light
let lightObj = new Object3D();
lightObj.rotationX = -45;
let light = lightObj.addComponent(DirectLight);
light.lightColor = new Color(1.0, 1.0, 1.0, 1.0);
light.intensity = 1;
scene.addChild(lightObj);
// add an Atmospheric sky enviroment
scene.addComponent(AtmosphericComponent).sunY = 0.6;
// create a view with target scene and camera
let view = new View3D();
view.scene = scene;
view.camera = mainCamera;
// start render
Engine3D.startRenderView(view);
const imageCanvas = document.createElement('canvas');
const context = imageCanvas.getContext('2d') as CanvasRenderingContext2D;
{
imageCanvas.width = imageCanvas.height = 128;
context.fillStyle = '#444';
context.fillRect(0, 0, 128, 128);
context.fillStyle = '#fff';
context.fillRect(0, 0, 64, 64);
context.fillRect(64, 64, 64, 64);
}
const image = imageCanvas.toDataURL('image/png');
let texture = new BitmapTexture2D();
texture.useMipmap = true;
await texture.load(image);
let mat = new UnLitMaterial();
mat.baseMap = texture;
mat.setUniformVector4('transformUV1', new Vector4(0, 0, 100, 100));
let plane = new PlaneGeometry(1000, 1000, 10, 10);
let planeObj = new Object3D();
let mr = planeObj.addComponent(MeshRenderer);
mr.geometry = plane;
mr.material = mat;
scene.addChild(planeObj);
let select = document.createElement('select');
select.innerHTML = `
<option value="true">Use MipMap</option>
<option value="false">No MipMap</option>
`;
select.setAttribute('style', 'position:fixed;right:5px;top:5px');
document.body.appendChild(select);
select.addEventListener('change', () => {
texture.useMipmap = select.value === 'true';
});