Эта статья является частью серии статей о three.js. Первая статья - основы Three.js. Предыдущая статья была о настройках окружения для этой статьи. Если вы их еще не читали, советую вам сделать это.
Текстуры - это своего рода большая тема в Three.js, и я не уверен на 100%, на каком уровне их объяснить, но я постараюсь. По ним есть много тем, и многие из них взаимосвязаны, поэтому трудно объяснить все сразу. Вот краткое содержание этой статьи.
Текстуры, как правило представляют собой изображения, которые чаще всего создаются в какой-либо сторонней программе, такой как Photoshop или GIMP. Например, давайте поместим это изображение на куб.
Мы изменим один из наших первых примеров. Все, что нам нужно сделать, это создать TextureLoader
. Вызовите
load
метод с URL-адресом изображения и установите для
изображения и установите его возвращаемое значение для map
свойства материала, вместо установки color
.
+const loader = new THREE.TextureLoader(); const material = new THREE.MeshBasicMaterial({ - color: 0xFF8844, + map: loader.load('../resources/images/wall.jpg'), });
Обратите внимание, что мы используем MeshBasicMaterial
поэтому не нужно никаких источников света.
Как насчет 6 текстур, по одной на каждой грани куба?
Мы просто создадим 6 материалов и передаем их в виде массива при создании Mesh
const loader = new THREE.TextureLoader(); -const material = new THREE.MeshBasicMaterial({ - map: loader.load('../resources/images/wall.jpg'), -}); +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')}), +]; -const cube = new THREE.Mesh(geometry, material); +const cube = new THREE.Mesh(geometry, materials);
Оно работает!
Однако следует отметить, что по умолчанию единственной геометрией, которая поддерживает
несколько материалов, является BoxGeometry
и ConeGeometry
. В других случаях вам
нужно будет создать или загрузить пользовательскую геометрию и/или изменить координаты
текстуры. Гораздо более распространенным является использование
Текстурного атласа
когда вы хотите разрешить несколько изображений для одной геометрии.
Что такое координаты текстуры? Это данные, добавленные к каждой вершине геометрического фрагмента, которые определяют, какая часть текстуры соответствует этой конкретной вершине. Мы рассмотрим их, когда начнем создавать собственную геометрию.
Большая часть кода на этом сайте использует самый простой способ загрузки текстур.
Мы создаем TextureLoader
и затем вызываем load
метод.
Возвращающий объект Texture
.
const texture = loader.load('../resources/images/flower-1.jpg');
Важно отметить, что при использовании этого метода наша текстура будет прозрачной, пока изображение не будет загружено асинхронно с помощью three.js, после чего он обновит текстуру загруженным изображением.
Это имеет большое преимущество в том, что нам не нужно ждать загрузки текстуры, и наша страница начнет отрисовку немедленно. Это, вероятно, хорошо для очень многих случаев использования, но если мы хотим, мы можем попросить three.js сообщить нам, когда текстура закончила загрузку.
Чтобы дождаться загрузки текстуры, load
метод загрузчика текстуры принимает
обратный вызов, который будет вызван после завершения загрузки текстуры.
Возвращаясь к нашему верхнему примеру, мы можем дождаться загрузки текстуры,
прежде чем создавать нашу Mesh
и добавлять ее в сцену следующим образом.
const loader = new THREE.TextureLoader(); loader.load('../resources/images/wall.jpg', (texture) => { const material = new THREE.MeshBasicMaterial({ map: texture, }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cubes.push(cube); // добавляем в наш список кубиков для вращения });
Если вы не очистите кеш вашего браузера и у вас не будет медленного соединения, вы вряд ли увидите разницу, но будьте уверены, что она ожидает загрузки текстуры.
Чтобы дождаться загрузки всех текстур, вы можете использовать LoadingManager
.
Создайте его и передайте его в TextureLoader
, а затем установите его onLoad
свойство для обратного вызова.
+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 = () => { + const cube = new THREE.Mesh(geometry, materials); + scene.add(cube); + cubes.push(cube); // add to our list of cubes to rotate +};
LoadingManager
также имеет onProgress
свойство и его
можно установить в другой функции обратного вызова, чтобы показать индикатор прогресса.
Сначала мы добавим индикатор выполнения (progress bar) в HTML
<body> <canvas id="c"></canvas> + <div id="loading"> + <div class="progress"><div class="progressbar"></div></div> + </div> </body>
и CSS для этого
#loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; } #loading .progress { margin: 1.5em; border: 1px solid white; width: 50vw; } #loading .progressbar { margin: 2px; background: white; height: 1em; transform-origin: top left; transform: scaleX(0); }
Затем в коде мы обновим масштаб (scale) progressbar
в onProgress
.
Он вызывается с URL-адресом последнего загруженного элемента, количества загруженных
элементов и общего количества загруженных элементов.
+const loadingElem = document.querySelector('#loading'); +const progressBarElem = loadingElem.querySelector('.progressbar'); loadManager.onLoad = () => { + loadingElem.style.display = 'none'; const cube = new THREE.Mesh(geometry, materials); scene.add(cube); cubes.push(cube); // добавляем в наш список кубиков для вращения }; +loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => { + const progress = itemsLoaded / itemsTotal; + progressBarElem.style.transform = `scaleX(${progress})`; +};
Если вы не очистите свой кеш и у вас медленное соединение, вы можете не увидеть полосу загрузки.
Чтобы использовать изображения с других серверов, эти сервера должны отправлять правильные заголовки. Если этого не произойдет, вы не сможете использовать изображения в three.js и получите ошибку. Если вы запускаете сервер, предоставляющий изображения, убедитесь, что он отправляет правильные заголовки. Если вы не управляете сервером, на котором размещены изображения, и он не отправляет заголовки разрешений, вы не сможете использовать изображения с этого сервера.
Например imgur, flickr и github - все заголовки отправки, позволяющие вам использовать изображения, размещенные на их серверах в three.js. Большинство других сайтов этого не делают.
Текстуры часто являются частью приложения three.js, которое использует больше всего памяти.
Важно понимать, что обычно, текстуры занимают width * height * 4 * 1.33
байт в памяти.
Обратите внимание, что никто не говорит о сжатии. Я могу сделать изображение в формате .jpg и установить его компрессию очень высокой. Например, допустим, я делал сцену из дома. Внутри дома есть стол, и я решил положить эту текстуру дерева на верхнюю поверхность стола.
Это изображение всего 157 Кб, поэтому оно будет загружаться относительно быстро, но на самом деле оно имеет размер 3024 x 3761 пикселей. Следуя приведенному выше уравнению, это
3024 * 3761 * 4 * 1.33 = 60505764.5
Это изображение займет 60 Мб ПАМЯТИ! в three.js. Несколько таких текстур, и вам не хватит памяти.
Я поднял этот вопрос, потому что важно знать, что использование текстур имеет скрытую стоимость. Для того чтобы three.js использовал текстуру, он должен передать ее в графический процессор, а графический процессор обычно требует, чтобы данные текстуры были несжатыми.
Мораль этой истории: делайте ваши текстуры небольшими по размеру, а не просто маленькими по размеру файла. Небольшой размер файла = быстрая загрузка. Маленький в размер = занимает меньше памяти. Насколько маленькими вы должны сделать их? Настолько, насколько это возможно! И при этом выглядящими так хорошо, как вам нужно.
Это почти то же самое, что и обычный HTML, поскольку JPG-файлы имеют сжатие с потерями, PNG-файлы имеют сжатие без потерь, поэтому PNG-файлы обычно загружаются медленнее. Но PNG поддерживают прозрачность. PNG также, вероятно, является подходящим форматом для данных без реалистичных изображений, таких как карты нормалей, и других видов карт без реалистичных изображений, которые мы рассмотрим позже.
Важно помнить, что JPG не использует меньше памяти, чем PNG в WebGL. Смотри выше.
Давайте применим эту текстуру 16x16
Это куб
Давайте нарисуем этот кубик действительно маленьким
Хммм, я думаю, это трудно увидеть. Давайте увеличим этот крошечный куб
Как GPU узнает, какие цвета нужно сделать для каждого пикселя, который он рисует для крошечного куба? Что если куб был настолько мал, что его размер составлял всего 1 или 2 пикселя?
Вот что такое фильтрация.
Если бы это был Photoshop, Photoshop усреднил бы почти все пиксели вместе, чтобы выяснить, какой цвет сделать эти 1 или 2 пикселя. Это было бы очень медленной операцией. Графические процессоры решают эту проблему с помощью mipmaps.
Mips - это копии текстуры, каждая из которых в два раза меньше ширины и в два раза меньше, чем предыдущий мип, где пиксели были смешаны, чтобы сделать следующий меньший мип. Мипы создаются до тех пор, пока мы не доберемся до 1 х 1 пикселя. Поскольку изображение выше всех мипов в конечном итоге будет что-то вроде этого
Теперь, когда куб нарисован настолько маленьким, что его размер составляет всего 1 или 2 пикселя, графический процессор может использовать только наименьший или почти минимальный уровень мипа, чтобы решить, какой цвет создать крошечный куб.
В three вы можете выбрать, что будет происходить, когда текстура рисуется больше, чем ее исходный размер, и что происходит, когда она рисуется меньше, чем ее исходный размер.
Для установки фильтра, когда текстура рисуется больше исходного размера,
вы устанавливаете texture.magFilter
свойство либо на THREE.NearestFilter
либо на
THREE.LinearFilter
. NearestFilter
просто выбрает один пиксель из оригинальной текстуры.
С текстурой низкого разрешения это дает вам очень пиксельный вид как Minecraft.
LinearFilter
выбрает 4 пикселя из текстуры, которые находятся ближе всего к тому месту,
где мы должны выбирать цвет, и смешает их в соответствующих пропорциях относительно того,
как далеко фактическая точка находится от каждого из 4 пикселей.
Для настройки фильтра, когда текстура нарисована меньше исходного размера,
вы устанавливаете для свойства texture.minFilter
одно из 6 значений.
THREE.NearestFilter
так же, как и выше. Выбирает ближайший пиксель в текстуре
THREE.LinearFilter
Как и выше, выбирает 4 пикселя из текстуры и смешает их
THREE.NearestMipmapNearestFilter
выбирает соответствующий mip, затем выбирает один пиксель.
THREE.NearestMipmapLinearFilter
выбирает 2 mips, выбирает один пиксель из каждого, смешает 2 пикселя.
THREE.LinearMipmapNearestFilter
выбирает подходящий mip, затем выбирает 4 пикселя и смешает их.
THREE.LinearMipmapLinearFilter
выбирает 2 mips, выбирает 4 пикселя от каждого и смешает все 8 в 1 пиксель.
Вот пример, показывающий все 6 настроек
Одна вещь, на которую нужно обратить внимание - это использование левого верхнего и верхнего среднего
NearestFilter
и LinearFilter
не использует mips. Из-за этого они мерцают на расстоянии, потому что графический процессор выбирает
пиксели из исходной текстуры. Слева выбран только один пиксель, а в середине 4 выбраны и смешаны,
но этого недостаточно, чтобы придумать хороший представительный цвет. Другие 4 полоски лучше с нижним правым,
LinearMipmapLinearFilter
лучший.
Если вы нажмете на картинку выше, она переключится между текстурой, которую мы использовали выше, и текстурой, где каждый уровень мипа имеет свой цвет.
Это делает более понятным, что происходит. Вы можете видеть в верхнем левом и верхнем середине первый мип, используемый на всем пути. Справа вверху и внизу посередине видно, где используется другой мип.
Возвращаясь к исходной текстуре, вы можете видеть, что нижний правый угол является самым плавным и с высочайшим качеством. Вы можете спросить, почему не всегда использовать этот режим. Самая очевидная причина - иногда вы хотите, чтобы вещи были пикселированы в стиле ретро или по какой-то другой причине. Следующая наиболее распространенная причина заключается в том, что чтение 8 пикселей и их смешивание медленнее, чем чтение 1 пикселя и смешивание. Хотя маловероятно, что для одной и той же текстуры будет видна разница в скорости по мере нашего углубления в эти статьи, в конечном итоге у нас будут материалы, которые используют 4 или 5 текстур одновременно. 4 текстуры * 8 пикселей на текстуру - это поиск 32 пикселей для каждого пикселя. Это может быть особенно важно учитывать на мобильных устройствах.
Текстуры имеют настройки для повторения, смещения и поворота текстуры.
По умолчанию текстуры в three.js не повторяются. Чтобы установить, повторяется или нет текстура,
есть 2 свойства: wrapS
для горизонтального и
и wrapT
вертикального повторения.
They can be set to one of:
THREE.ClampToEdgeWrapping
Последний пиксель на каждом ребре повторяется всегда
THREE.RepeatWrapping
Текстура повторяется
THREE.MirroredRepeatWrapping
Текстура зеркально повторяется.
Например, чтобы включить повтор в обоих направлениях:
someTexture.wrapS = THREE.RepeatWrapping; someTexture.wrapT = THREE.RepeatWrapping;
Повторение устанавливается с помощью свойства [repeat].
const timesToRepeatHorizontally = 4; const timesToRepeatVertically = 2; someTexture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically);
Смещение текстуры может быть сделано путем установки offset
.
Текстуры смещены в единицах, где 1 единица = 1 размер текстуры.
Другими словами, 0 = без смещения и 1 = смещение на одну полную величину текстуры.
const xOffset = .5; // cмещение на половину текстуры const yOffset = .25; someTexture.offset.set(xOffset, yOffset);`
Вращение текстуры может быть установлено через свойство rotation
в радианах, а также свойство
center
для выбора центра вращения. По умолчанию используется значение 0,0, которое
вращается из нижнего левого угла. Подобно смещению, эти единицы имеют размер текстуры,
поэтому установка их .5, .5
будет вращаться вокруг центра текстуры.
someTexture.center.set(.5, .5); someTexture.rotation = THREE.MathUtils.degToRad(45);
Давайте изменим верхний пример выше, чтобы играть с этими значениями
Сначала мы сохраним ссылку на текстуру, чтобы мы могли манипулировать ею
+const texture = loader.load('../resources/images/wall.jpg'); const material = new THREE.MeshBasicMaterial({ - map: loader.load('../resources/images/wall.jpg'); + map: texture, });
Затем мы снова будем использовать lil-gui для обеспечения простого интерфейса.
import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
Как мы делали в предыдущих примерах lil-gui, мы будем использовать простой класс, чтобы дать lil-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
, а lil-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 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');
Последнее, что следует отметить в этом примере, это то, что если вы измените wrapS
или
wrapT
на текстуре, вы также должны установить texture.needsUpdate
так, чтобы Three.js знал, чтобы применить эти настройки. Другие настройки применяются автоматически.
Это только один шаг в тему текстур. В какой-то момент мы рассмотрим текстурные координаты, а также 9 других типов текстур, которые можно применить к материалам.
А пока давайте перейдем к свету.