html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>09</title>
<link rel="stylesheet" type="text/css" href="09.css">
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r125/build/three.js"></script>
<script src="https://threejsfundamentals.org/3rdparty/dat.gui.min.js"></script>
</head>
<body>
<canvas id="c"></canvas>
<script src="09.js"></script>
</body>
</html>
css
html, body {
height: 100%;
margin: 0;
}
#c {
width: 100%;
height: 100%;
display: block;
}
js
/*
* 纹理
* 不是所有的几何体类型都支持多种材质
* BoxGeometry 可以使用 6 种材质,每个面一个
* ConeGeometry 可以使用 2 种材质,一个用于底部,一个用于侧面
* CylinderGeometry 可以使用 3 种材质,分别是底部,顶部和侧面
* 对于其他情况, 需要自定义几何体或者修改纹理坐标
*
* 纹理的加载
*
* 一、简单的方法: 先渲染几何体,等纹理加载完成后再渲染纹理
* 创建一个 TextureLoader 对象,调用它的 load 方法,这将返回一个 Texture 对象
* const loader = new THREE.TextureLoader();
* const texture = loader.load('resources/images/pic.jpg');
* const material = new THREE.MeshBasicMaterial({map: texture});
* const cube = new THREE.Mesh(geometry, material);
* 需要注意的是,使用 load 方法,我们的纹理将是透明的,直到图片被 three.js 异步加载完成,这时它将用图片更新纹理
* 这有一个很大的好处,就是我们不必等待纹理加载,我们的页面会立即开始渲染。
*
* 二、等待纹理加载: 先等纹理加载成功后,再一同渲染几何体
* const loader = new THREE.TextureLoader();
* loader.load('resources/images/pic.jpg', (texture) => {
* const material = new THREE.MeshBasicMaterial({
* map: texture;
* });
* const cube = new THREE.Mesh(geometry, material);
* scene.add(cube);
* cubes.push(cube);
* });
* 为了等待贴图加载,贴图加载器的 load 方法会在贴图加载完成后调用一个回调
* 在 load 的回调函数中,我们再去创建几何体,以及添加到场景中,这样就能达到渲染几何体时,所有的纹理都已经加载好了
*
* 以上两种方法,除非清理了浏览器的缓存或者链接非常缓慢,否则看不到很大的差异
*
* 对于方法二,若要实现等待多个纹理加载结束后再渲染,则需要使用 LoadingManager。
* const loadManager = new THREE.LoadingManager();
* const loader = new THREE.TextureLoader(loadManager);
* const materials = [
* new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}),
* new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}),
* new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}),
* new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}),
* new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}),
* new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}),
* ];
*
* // 将 loadManager 的 onLoad 属性设置为回调
* loadManager.onLoad = () => {
* const cube = new THREE.Mesh(geometry, materials);
* scene.add(cube);
* cubes.push(cube); // 添加到我们要旋转的立方体数组中
* };
*
* LoadingManager 也有一个 onProgress 属性,我们可以设置为另一个回调来显示进度指示器。
* */
/*
* 内存管理
* 纹理往往是three.js应用中使用内存最多的部分。重要的是要明白,一般来说,纹理会占用 宽度 * 高度 * 4 * 1.33 字节的内存。
* 注意,这里没有提到任何关于压缩的问题。我可以做一个.jpg的图片,然后把它的压缩率设置的超级高。
* 比如说我在做一个房子的场景。在房子里面有一张桌子,我决定在桌子的顶面放上这个木质的纹理
* 图片只有157k,所以下载起来会比较快,但实际上它的大小是3024×3761像素.。按照上面的公式,那就是
* 3024 * 3761 * 4 * 1.33 = 60505764.5
* 在three.js中,这张图片会占用60兆(meg)的内存!。只要几个这样的纹理,你就会用完内存。
*
* 这个故事的寓意在于,不仅仅要让你的纹理的文件大小小,还得让你的纹理尺寸小。
* 文件大小小=下载速度快。尺寸小=占用的内存少。你应该把它们做得多小?越小越好,而且看起来仍然是你需要的样子。
* */
/*
* 过滤(filtering)
* GUP 如何知道每一个像素需要使用哪些颜色呢? GUP 使用 mipmaps 解决该问题。
* Mips 是纹理的副本,每一个都是前一个 mip 的一半宽和一半高,其中的像素已经被混合以制作下一个较小的 mip。Mips一直被创建,直到我们得到1x1像素的Mip。
*
* 1、当纹理绘制的尺寸大于其原始尺寸时,可以设置 texture.magFilter 属性
* THREE.NearestFilter : 意味着只需从原始纹理中选取最接近的一个像素。对于低分辨率的纹理,这给你一个非常像素化的外观
* THREE.LinearFilter : 是指从纹理中选择离我们应该选择颜色的地方最近的4个像素,并根据实际点与4个像素的距离,以适当的比例进行混合
*
* 2、当纹理绘制的尺寸小于其原始尺寸时, 可以设置 texture.minFilter 属性
* THREE.NearestFilter : 在纹理中选择最近的像素
* THREE.LinearFilter : 从纹理中选择 4 个像素,并混合它
* THREE.NearestMipmapNearestFilter : 选择合适的mip,然后选择一个像素。
* THREE.NearestMipmapLinearFilter : 选择2个mips,从每个mips中选择一个像素,混合这2个像素。
* THREE.LinearMipmapNearestFilter : 选择合适的mip,然后选择4个像素并将它们混合。
* THREE.LinearMipmapLinearFilter : 选择2个mips,从每个mips中选择4个像素,然后将所有8个像素混合成1个像素。
*
* */
/*
* 纹理有重复,偏移和旋转的设置
*
* 1、重复,使用 texture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically); 设置
* 默认情况下,three.js 中纹理是不重复的
* 要设置纹理是否重复,有 2 个属性
* wrapS 用于水平方向
* wrapT 用于垂直方向
* 其值是下列的一个
* THREE.ClampToEdgeWrapping 每条边上最后一个像素无限重复
* THREE.RepeatWrapping 纹理重复,比如设置成该方式,且重复次数(timesToRepeat*)设置成2,则会在该平面画两个该纹理。
* THREE.MirroredRepeatWrapping 每次重复时进行镜像
*
* 2、偏移,使用 texture.offset.set(xOffset, yOffset); 设置
* 偏移的显示效果也与 wrapS 和 wrapT 的值有关系,若是 ClampToEdgeWrapping 则偏移的部分就是像素拉长,若是 RepeatWrapping 则偏移的部分就是纹理的一部分
* 纹理的偏移是以纹理为单位的,即偏移 0 表示没有偏移, 偏移 1 表示偏移一个完整的纹理数量
*
* 3、旋转,
* texture.center.set(.5, .5); // 设置旋转中心
* texture.rotation = THREE.MathUtils.degToRed(45);
* */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2;
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const boxGeometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const cubes = [];
const loader = new THREE.TextureLoader();
const texture = loader.load('https://threejsfundamentals.org/threejs/resources/images/wall.jpg');
const material = new THREE.MeshBasicMaterial({map: texture});
const cube = new THREE.Mesh(boxGeometry, material);
scene.add(cube);
cubes.push(cube);
/*
* 使用一个简单的类来给dat.GUI提供一个可以以度数为单位进行操作的对象,让它以弧度为单位设置该属性。
* */
class DegRadHelper {
constructor (obj, prop) {
this.obj = obj;
this.prop = prop;
}
get value() {
return THREE.MathUtils.radToDeg(this.obj[this.prop]);
}
set value(v) {
this.obj[this.prop] = THREE.MathUtils.degToRad(v);
}
}
/*
* 字符串转数字,将 "123" 这样的字符串转换为 123 这样的数字,因为three.js的枚举设置需要数字,比如 wrapS 和 wrapT,但dat.GUI只使用字符串来设置枚举。
* */
class StringToNumberHelper {
constructor (obj, prop) {
this.obj = obj;
this.prop = prop;
}
get value() {
return this.obj[this.prop];
}
set value(v) {
this.obj[this.prop] = parseFloat(v);
}
}
/*
* 纹理重复的类型
* */
const wrapModes = {
'ClampToEdgeWrapping': THREE.ClampToEdgeWrapping,
'RepeatWrapping': THREE.RepeatWrapping,
'MirroredRepeatWrapping': THREE.MirroredRepeatWrapping,
};
function updateTexture() {
texture.needsUpdate = true;
}
const gui = new dat.GUI();
gui.add(new StringToNumberHelper(texture, 'wrapS'), 'value', wrapModes).name('texture.wrapS').onChange(updateTexture);
gui.add(new StringToNumberHelper(texture, 'wrapT'), 'value', wrapModes).name('texture.wrapT').onChange(updateTexture);
gui.add(texture.repeat, 'x', 0, 5, .01).name('texture.repeat.x');
gui.add(texture.repeat, 'y', 0, 5, .01).name('texture.repeat.y');
gui.add(texture.offset, 'x', -2, 2, .01).name('texture.offset.x');
gui.add(texture.offset, 'y', -2, 2, .01).name('texture.offset.y');
gui.add(texture.center, 'x', -.5, 1.5, .01).name('texture.center.x');
gui.add(texture.center, 'y', -.5, 1.5, .01).name('texture.center.y');
gui.add(new DegRadHelper(texture, 'rotation'), 'value', -360, 360).name('texture.rotation');
function resizeRenderToDisplaySize(renderer) {
const canvas = renderer.domElement;
const pixelRatio = window.devicePixelRatio;
const width = canvas.clientWidth * pixelRatio | 0;
const height = canvas.clientHeight * pixelRatio | 0;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRenderToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cubes.forEach((cube, ndx) => {
const speed = .2 + ndx * .1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();