предисловие
Обновление от 23 января 2022 г.: из-за проблем с интеллектуальной собственностью исходный код навсегда удален из личного GitHub, и в статье по-прежнему много фрагментов кода.
Во время стажировки автор реализовал демонстрационную демонстрацию фанатской сцены Web3D (использовалась для защиты стажировки, которой в то время не было в сети). В основном правильноФан-сцена Тупуулучшение.
Это может быть из-за того, что это всего лишь демо-версия официального сайта Tupu, поэтому он может быть запущен в Интернете в спешке, поэтому у фан-сцены Tupu есть следующие проблемы.
- Медленная загрузка в первый раз (в основном связанная с 3D-моделями)
- Неэффективный рендеринг (в основном связанный с 3D-моделями)
- Невозможность сцены (невозможно изменить погоду, анимацию и т. д.)
- Плохая интерактивность (практически никакой интерактивности)
Итак, я внес следующие улучшения в демонстрацию Tupu.
- Увеличьте сцену - измените скорость лопасти в соответствии с изменением скорости ветра,Моделирование погоды в поле ветра на основе прогноза погоды
- Повысьте эффективность рендеринга — используйте модель формата glTF.
- Расширенная интерактивность - расширенные возможности управления, выбор предоставляется пользователю
- Демонстрационное видео -Демонстрационное видео фанатской 3D сцены __ Bilibili
Основы Web3D
Базовые знания 3D в основном основаны на компьютерной графике.
Включая сцену Сцена, камера Камера, рендерер рендерера, геометрия Геометрия, модель освещения Свет, карта текстур Текстура, материал Материал и т. д.
Конкретный может относиться кБлог Го Лунбанга, контента действительно много, поэтому я не буду его здесь раскрывать.
выбор 3D модели
Выбор формата 3D-модели — самый простой шаг, но, вероятно, самый важный для эффективности рендеринга.
3D-формат Tupu — OBJ+MTL, что также является одним из важных факторов низкой скорости загрузки и высокой нагрузки на рендеринг, поэтому я начну с формата 3D-модели и сделаю первое улучшение.
Разница между часто используемыми моделями Web3D
Прежде чем выбрать 3D-формат, нам нужно понять, какие основные 3D-форматы доступны и каковы характеристики каждого из них.
- glTF
glTF — это очень всеобъемлющий формат, который поддерживает почти все распространенные функции, поддерживая важные функции, такие как цвет, анимация, CSG, работа с детальной сеткой, текстуры, камеры, источники света, относительное позиционирование и т. д.
glTF — это транзитный формат, для него не нужно создавать импортеры и конвертеры, как для других форматов, и он может напрямую использоваться соответствующими приложениями.
- STL
STL поддерживает только простое выражение функции геометрии и других свойств, таких как цвет, материал, освещение, текстура и т. Д. Недоступны, поэтому он почти не используемый формат 3D
- OBJ
OBJ поддерживает определенные функции, такие как краткое выражение, CSG, цвет и материал. Но нет важных свойств 3D-модели, таких как анимация, текстуры, камеры и т. д. Например, для моделей в формате OBJ, если нужно отобразить нужную текстуру, нужно использовать дополнительный файл mtl для отображения текстуры.
- FBX
Точнее, модель FBX — это формат модели общего назначения, а не формат файла 3D-модели, такой как OBJ, поддерживающий все основные элементы 3D-данных, а также 2D-элементы, аудио- и видеомедиа-элементы, а также анимацию, свойства материалов, текстуры, скелетная анимация, освещение, камера и другая информация.
Формат FBX в основном поддерживает игровые модели полигонов, кривые, поверхности и материалы группы точек. А вот детальной сетки (Detailed Mesh) нет, другие так же совершенны, как и gltf, который больше подходит для игр.
Сводка диаграммы
Почему я использую формат glTF
В основе glTF по-прежнему лежит множество принципов работы. Здесь я просто объясню некоторые из его преимуществ как формата Web3D. Если вам нужно подробно изучить glTF, вы можете нажать glTF-Tutorialsа такжеКак быстро отображать 3D модели на странице
- Устраняет дополнительное преобразование данных модели и повышает скорость синтаксического анализа.
- glTF стремится быть транзитным форматом, а не еще одним новым форматом 3D-данных.
- Используйте JSON для описания структуры сцены, которая может быть легко проанализирована и обработана приложением. Но данные геометрии и данные текстуры 3D-объектов обычно не включаются в файлы JSON, они хранятся во внешних файлах или двоичных файлах, а файлы JSON содержат только ссылки на эти внешние файлы.
- 3D-данные хранятся (в двоичной форме) таким образом, что большинство графических API могут напрямую использовать их без необходимости декодирования или предварительной обработки приложением.
Другие форматы, такие как OBJ, должны анализировать структуру сцены, чтобы читать различные форматы входных файлов перед запуском соответствующего приложения, и должны преобразовывать числовые данные геометрии в формат, требуемый графическим API. Следовательно, каждое приложение среды выполнения должно создавать множество импортеров и конвертеров для всех поддерживаемых форматов файлов, что не только занимает память, но и снижает эффективность рендеринга.
- Легче изменить анимацию
- Демонстрация фан-сцены Tupu основана на анимации в формате OBJ, но поскольку OBJ не имеет собственной анимации, изменить анимацию будет сложнее.
- Модифицировать анимацию glTF очень удобно, о деталях анимации я продолжу рассказывать ниже.
Изменить анимацию 3D-модели glTF
Скорость лопастей вентилятора Tupu фиксирована. Я надеюсь, что она может быть основана на сцене и соответственно изменяться в соответствии с данными о скорости ветра с задней части. То есть, чем быстрее скорость ветра, тем быстрее вращаются лопасти. Это предполагает изменение модели анимации.
Выясните, как glTF анимирует
3D модели в формате glTF могут иметь собственную анимацию (это сильно отличается от OBJ, OBJ не может иметь свою анимацию)
Для базового хранения анимации формата GLTF и принципов интерполяции анимации, пожалуйста, смотритеПодробное объяснение формата glTF (пример: простая анимация)а такжеПодробное объяснение формата glTF (анимация)
Если вы не хотите углубляться, можете пропустить две приведенные выше ссылки, в этой статье речь идет только о том, как изменить анимацию после того, как загрузчик Three.js загрузит 3D-модель в формате glTF, сосредоточившись на реализации, не расширяя принцип.
Изменить анимацию модели
- Сначала загрузите модель с анимацией через GLTFLoader.
import turbine from '../../assets/3d-gltf-model/turbine.glb';
loadTurbine = () => {
const loader = new GLTFLoader()
if (this.scene.getObjectByName('turbine')) {
let removeTurbine = this.scene.getObjectByName('turbine')
this.scene.remove(removeTurbine)
}
loader.load(turbine, object => {
this.matrixTurbine = object;
let mesh = object.scene;
mesh.position.set(0, -2, 0);
this.scene.add(mesh);
})
}
- Загрузчик загружает свойства объекта MatrixTurbine
githubМодель glTF в примере имеет собственную анимацию, поэтому после формирования объекта после загрузки модели данные, связанные с анимацией, сохраняются в свойствах объекта matrixTurbine
- Свойство animations объекта matrixTurbine представляет собой массив, в котором хранится AnimationsClip, представляющий собой набор многократно используемых дорожек ключевых кадров, представляющих анимации, поставляемые с gltf.
Продолжительность в AnimationsClip — это продолжительность этой группы анимаций, а массив треков хранит пары «ключ-значение» кадров анимации в течение времени анимации, которое представляет собой набор треков ключевых кадров, то есть положение компонента, соответствующего каждому единица времени (поскольку модель glTF имеет два режима отображения, прозрачный и непрозрачный, поэтому массив дорожек на рисунке имеет два объекта QuaternionKeyframeTrack). Повторяйте этот набор дорожек ключевых кадров непрерывно, чтобы получить непрерывную анимацию.
- свойство times и values объекта QuaternionKeyframeTrack
Свойство times объекта QuaternionKeyframeTrack представляет собой временной массив ключевых кадров, свойство values объекта QuaternionKeyframeTrack представляет собой массив значений, соответствующих точкам времени в массиве времени.
Как понять это?
Давайте сначала распечатаем эти два свойства, чтобы увидеть, что они есть. Мы видим, что длина раз - четверть значения
console.log(this.matrixTurbine.animations[0].tracks[1].times)
console.log(this.matrixTurbine.animations[0].tracks[1].values)
// Float32Array(145)
// Float32Array(580)
Мы можем представить, что атрибут values представляет собой набор действий, которые необходимо выполнить 3D-объектам, а times — это момент, когда эти действия будут завершены.Например, в приведенном выше примере каждый элемент массива each times соответствует четырем элементам в значениях, через внутренний glTF Алгоритм интерполяции анимации для формирования полной непрерывной анимации. Конкретный принцип, возможно, придется написать в другой статье, здесь я пока выкопаю яму и пойму ее визуально.
- Измените скорость анимации, изменив массив атрибутов времени.
Ранее мы упоминали о свойстве продолжительности анимаций, которое представляет собой продолжительность цикла анимации Массив времен на самом деле представляет собой четное деление времени. Например, продолжительность = 6, в это времяFloat32Array(145)
То есть [0, 0,0416666679084301, 0,0833333358168602, 0,125 ...... 5,958333492279053, 6].
Если я хочу изменить скорость анимации, мне нужно только изменить массив времени и продолжительность пропорционально скорости ветра WindSpeed из бэкэнда.
// 改变风机转速的部分
this.matrixTurbine.animations[0].tracks[1].times = this.changeArr(object.animations[0].tracks[1].times)// 控制透明的风机动画速度
this.matrixTurbine.animations[0].tracks[0].times = this.changeArr(object.animations[0].tracks[0].times) // 控制有材质的风机动画速度
this.matrixTurbine.animations[0].duration = 6 / (windSpeed / 5)
function changeArr(arr){
return arr.map((a) => a / (windSpeed / 5))
}
Добавить анимацию в модель gltf без анимации
Этот пример в основном говорит о том, как изменить glTF-модель с анимацией.Если вы столкнулись с 3D-моделью без анимации, вы можете попробовать операцию:
Вместо того, чтобы создавать экземпляры AnimationClips напрямую с помощью конструктора, вы можете создавать AnimationClips с помощью одного из его статических методов: из JSON (анализ), из целевых последовательностей морфинга (CreateFromMorphTargetSequence, CreateClipsFromMorphTargetSequences) или из иерархии анимации (parseAnimation) — если ваша модель еще не сохранена. AnimationClips в массиве анимации своей геометрии.
Моделирование грозовой погоды
Еще одним требованием является визуализация погоды в соответствии с внутренним интерфейсом данных о погоде.Я буду использовать погоду с грозой в качестве примера, чтобы расширить знания о текстурах, материалах и геометрии.
Демонстрация грозовой погоды
Мы видим, что время от времени будут молнии, а облака плотные и струящиеся.
Облачная реализация
Реализация в основном связана с геометрией, текстурой и материалом.
- На основе трехмерных пространственных координат создайте несколько случайных плоских геометрий над вентилятором и настройте нефизический материал, который вычисляет только диффузное отражение и фокусируется на ламбертиане света.
Мы должны сначала знать базовые знания 3D: меш (Mesh) состоит из геометрии и материалов, а Объект всей 3D-сцены можно приблизительно рассматривать как комбинацию нескольких мешей. Темное облако можно использовать как сетку, состоящую из геометрии и материалов. Облако в коде — это Mesh, составленный из геометрии и материалов, а значит, основанный на треугольниках.polygon mesh(полигональная сетка) класс объекта.
Очевидно, геометрические свойства Mesh можно аппроксимировать как плоскость, а несколько плоскостных геометрий PlaneBufferGeometry случайным образом сплющиваются, образуя облачный слой на небе, как геометрический носитель «темных облаков».
Поскольку само темное облако не похоже на зеркальное отражение, когда его освещает молния, оно ближе к эффекту диффузного отражения, поэтому мы можем использовать конструктор материалов MeshLambertMaterial из Three.js для создания тусклой и не блестящей поверхности, которая в основном отражает диффузное отражение света.
// cloudGeo既是上面提到的几何体,cloudMaterial是包含了纹理(texture)的材质,纹理的性质我们在下文会讲到
const cloudGeo = new THREE.PlaneBufferGeometry(400, 400);
const cloudMaterial = new THREE.MeshLambertMaterial({
map: texture,
transparent: true
});
- Если свойства материала в Mesh должны играть визуальную роль, их необходимо комбинировать с текстурами, которые часто называют «картами».
пройти черезlet cloud = new THREE.Mesh(cloudGeo, cloudMaterial)
Эта строка кода, встроенное уравнение проекции и функция отображения фреймворка сопоставляют сетку с пространством текстуры, а затем пространство текстуры извлекает значение цвета дыма, другими словами, «приклеивает» smoke.png к создал сетку, содержащую геометрию и материалы.
- Установите положение нескольких сеток
В этом коде задаются 5 мешей, которые случайным образом размещаются в пространстве над 3D-веером, что выглядит как «покрытие» веера.Читатели могут использовать это для ознакомления с трехмерной системой координат Three.js при ее реализации сами по себе. .
for (let p = 0; p < 5; p++){
let cloud = new THREE.Mesh(cloudGeo, cloudMaterial);
cloud.position.set( Math.random() * 10 + 90, Math.random() * 20 + 15, -Math.random() * 50 - 80 );
}
Реализация облачного потока
rotation
: указывает радианы, в которых объект вращается вокруг осей x, y и z. В функции анимации все сетки темных облаков вращаются вокруг оси z, а затем для анимации используется процедура requestAnimationFrame, которая здесь повторяться не будет.
function animate() {
cloudParticles.forEach(p => { // cloudParticles 是包含 数个乌云Mesh 的数组
p.rotation.z -= 0.002;
});
}
requestAnimationFrame(animate);
Реализация грозовой молнии
Реализация освещения связана с моделью освещения.Типы освещения в Three.js в основном включают окружающий свет AmbientLight, параллельный свет DirectionalLight и точечный источник света PointLight.
- тип используемого освещения
Свет точечного источника света расходится, и его направление света нельзя определить напрямую, что аналогично световым характеристикам молнии, поэтому мы используем точечный источник света для имитации молнии.
При рендеринге каждого кадра анимации точечный источник света может генерироваться в случайном месте над сеткой темного облака.Источник света освещает темное облако и модель веера.Свет будет рассчитываться с геометрической текстурой и нормалью вершин вектор из двух, тем самым изменив большое количество вершин цвета, повторно войдите в конвейер рендеринга, чтобы имитировать эффект молнии
wholeFlashGroup.name = 'flash'
const flash = new THREE.PointLight(0xe0ffff, 10000, 0, 2);
flash.position.set(100, 100, -110);
wholeFlashGroup.add(flash);
function animate() {
if (Math.random() > 0.90 || flash.power > 220) {
if (flash.power < 100)
flash.position.set(
Math.random() * 30 + 80,
Math.random() * 20 + 10,
Math.random() * 3 - 100
);
flash.power = 50 + Math.random() * 500;
}
requestAnimationFrame(animate);
}
Резюме: Основной код раздела грозовой погоды
// 闪电乌云特效
loadFlash = () => {
const wholeFlashGroup = new THREE.Group();
wholeFlashGroup.name = 'flash'
const ambient = new THREE.AmbientLight(0x555555);
wholeFlashGroup.add(ambient);
const directionalLight = new THREE.DirectionalLight(0xffeedd);
directionalLight.position.set(0, 0, 1);
wholeFlashGroup.add(directionalLight);
const flash = new THREE.PointLight(0xe0ffff, 10000, 0, 2);
flash.position.set(100, 100, -110);
wholeFlashGroup.add(flash);
this.myRef.current.appendChild(renderer.domElement);
let loader = new THREE.TextureLoader();
loader.load(smoke, (texture) => {
const cloudGeo = new THREE.PlaneBufferGeometry(400, 400);
const cloudMaterial = new THREE.MeshLambertMaterial({//一种用于无光泽表面的材料,没有镜面高光。该材料使用基于非物理的朗伯模型来计算反射率
map: texture,
transparent: true
});
for (let p = 0; p < 5; p++) {
let cloud = new THREE.Mesh(cloudGeo, cloudMaterial);
cloud.position.set(
Math.random() * 10 + 90,
Math.random() * 20 + 15,
-Math.random() * 50 - 80
);
cloud.rotation.x = 1.16;
cloud.rotation.y = -0.12;
cloud.rotation.z = Math.random() * 360;
cloud.material.opacity = 0.4;
cloudParticles.push(cloud);
wholeFlashGroup.add(cloud);
}
this.scene.add(wholeFlashGroup)
animate();
});
const animate = () => {
cloudParticles.forEach(p => { // cloudParticles 是包含 数个乌云Mesh 的数组
p.rotation.z -= 0.002;
});
if (Math.random() > 0.90 || flash.power > 220) {
if (flash.power < 100)
flash.position.set(
Math.random() * 30 + 80,
Math.random() * 20 + 10,
Math.random() * 3 - 100
);
flash.power = 50 + Math.random() * 500;
}
flashAnimatation = requestAnimationFrame(animate);
}
}
Взаимодействие 3D-моделей glTF
Первый шаг взаимодействия: щелчок мышью для получения 3D-объекта
- Преобразование координат экрана мыши в стандартные координаты устройства
Получить 3D-объекты для взаимодействия не так просто, как 2D-плоскости.Основное противоречие заключается в том, что объекты модели существуют в виде 3D в памяти, но координаты наших щелчков мыши — это все 2D-координаты, основанные на оси XY на экране.
Таким образом, прежде всего, двумерная координата экрана положения щелчка мыши должна быть преобразована в стандартизированную координату устройства Three.js (процесс, обратный «Примечанию» ниже). Three.js может создать луч захвата с помощью стандартизация координат устройства для получения 3-х объектов в мерном пространстве.
Примечание: OpenGL ожидает, что все вершины, которые мы видим, станут нормализованными координатами устройства (NDC) после запуска всех вершинных шейдеров. То есть координаты в каждом направлении должны быть между -1.0 и 1.0, а вершины вне этого диапазона координат не будут видны. Обычно мы сами устанавливаем диапазон координат, а затем конвертируем эти координаты в нормализованные координаты устройства в вершинном шейдере. Эти нормализованные координаты устройства затем передаются в растеризатор, который преобразует их в 2D-координаты или пиксели на экране.
// 通过鼠标事件的返回的事件对象event的坐标属性clientX、clientY获得鼠标点击位置相对浏览器窗口的坐标
// 然后转化为标准设备坐标
this.mouse.x = (event.clientX / w) * 2 - 1;
this.mouse.y = -(event.clientY / h) * 2 + 1;
- Создайте объект, который выбирает лучи для пересечения
Выбор луча подобен вертелу, пересекающему трехмерное пространство, «нанизывающему» все трехмерные объекты, которые он пересекает, для формирования массива, и первым элементом массива обычно является объект, на который мы нажали.
пройти черезraycaster.setFromCamera(mouse, camera)
, т.е. нормализовать координаты мыши координат устройства и камеры для создания выбранного луча
пройти черезraycaster.intersectObject(equipment, true)
Вычисляет объекты, пересекаемые выбранным лучом, и возвращает все объекты, повернутые лучом.Параметром этого метода является массив объектов Object3D, указывающий диапазон выбора объекта луча.Все выбранные будут возвращены в виде Если координаты экрана двух сеток Если позиции совпадают, то все они будут выбраны
- Резюме: Щелкните мышью, чтобы получить код объекта 3D-модели.
function onPointerClick (event){
const [w, h] = [window.innerWidth, window.innerHeight];
const {mouse, equipment, raycaster} = this;
this.mouse.x = (event.clientX / w) * 2 - 1;
this.mouse.y = -(event.clientY / h) * 2 + 1;
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObject(equipment, true);
if (intersects.length <= 0) {
return false;
}
const selectedObject = intersects[0].object;
if (selectedObject.isMesh) {
// 交互效果相关的代码
}
}
Эффект взаимодействия с 3D-моделью
Здесь я в основном пишу реализацию одного из самых важных интерактивных эффектов: нажмите на каждый компонент, внешний край 3D-компонента будет излучать свет, и отобразите метку, чтобы отобразить его детали.
- Цепочка процессов постобработки EffectComposer
На самом деле существует много видов интерактивных эффектов, но все они требуют создания цепочки процессов постобработки EffectComposer, которая производит окончательный визуальный эффект, сначала рендерит исходную сцену, а затем накладывает дополнительные интерактивные эффекты рендеринга.
- новый EffectComposer( renderer : WebGLRenderer, renderTarget : WebGLRenderTarget ) используется для реализации эффектов постобработки в three.js. Класс EffectComposer управляет цепочкой процессов постобработки, создающих окончательный визуальный эффект. Проходы постобработки выполняются в том порядке, в котором они были добавлены/вставлены, и последний проход автоматически отображается на экране.
let compose = new EffectComposer(this.renderer);
compose.addPass(firstPass); // 过程链先处理 firstPass,渲染场景,否则会失去其他所有mesh
compose.addPass(secondPass); // 渲染完原场景,处理secondPass的渲染管道
- Контурное освещение 3D-деталей
Чтобы добавить краевой свет в 3D-модель, я использовалthree-outlinepass
библиотеку, сначала вызовите RenderPass для рендеринга исходной сцены, а затем выполните рендеринг layoutPass, добавляющий контурный свет.
-
новый RenderPass(сцена, камера), RenderPass — это одиночный запуск конвейера рендеринга. RenderPass визуализирует изображение во вложение кадрового буфера в памяти. В начале процесса рендеринга каждое вложение необходимо инициализировать в блочной памяти и, возможно, потребуется записать обратно в память в конце рендеринга.
-
Чтобы узнать об OutlinePass, вы можете обратиться кthree.js examples (OutlinePass)Эта демонстрация для обучения и не будет расширена здесь.
/*
import {RenderPass, EffectComposer, OutlinePass} from "three-outlinepass";
*/
function outline (selectedObjects, color = 0x15c5e8) {
const [w, h] = [window.innerWidth, window.innerHeight];
let compose = new EffectComposer(this.renderer);
let renderPass = new RenderPass(this.scene, this.camera);
let outlinePass = new OutlinePass(
new THREE.Vector2(w, h),
this.scene,
this.camera,
selectedObjects
);
outlinePass.renderToScreen = true;
outlinePass.selectedObjects = selectedObjects;
compose.addPass(renderPass); // 过程链先处理renderPass,渲染场景,不然除了outline高亮轮廓,否则会失去其他所有mesh
compose.addPass(outlinePass); // 渲染完原场景,处理outline管道
const params = {
edgeStrength: 3,
edgeGlow: 0,
edgeThickness: 20,
pulsePeriod: 1,
usePatternTexture: false
};
outlinePass.edgeStrength = params.edgeStrength;
outlinePass.edgeGlow = params.edgeGlow;
outlinePass.visibleEdgeColor.set(color);
outlinePass.hiddenEdgeColor.set(color);
compose.render(this.scene, this.camera);
this.compose = compose
}
заключительные замечания
Эта статья не слишком расширяет принцип, потому что в 3D слишком много принципов, которые нужно дополнить, что может нарушить непрерывность статьи, поэтому соответствующие учебные материалы представлены в виде ссылок в статье, пожалуйста, обратитесь самим читателям.
Я всегда чувствую, что 3D и VR станут очень важным направлением во фронтенде, надеюсь, эта статья сможет вам помочь.