скорее всего это последовательность
По поводу Three.js, в интернете не так много отрывочных туториалов, какие-то слишком простые, какие-то мутные, а примеры на официальном сайте Three.js слишком высокие и запредельные, это для тех, кто не изучал компьютерную графику .Для детской обуви это как порог входа,не знаю,наступать ли сначала левой ногой или правой ногой.Мне было интересно,поэтому я прыгнул ногами,чтобы проверить воду...
В этой статье будет описан процесс построения и поверхностные принципы 3D-мира Интернета максимально драматичным языком (потому что в данный момент я не понимаю глубинных принципов...), вот, пожалуйста.应该/可能/或许/大概/看造化
Узнаете (базовый):
- Процесс построения 3D-сцены
- Добавление объектов и импорт внешней модели
- Мышь взаимодействует с объектами на сцене
- Построение системы трехмерных частиц
- анимация частиц
- Часть принципа рендеринга шейдера
Честно говоря, чтобы привлечь всех к просмотру чиновников, менее многословных, сначала смотрите на вещи (гифка может быть чуть крупнее):
Можно наблюдать готовые эффекты: преобразование частиц, легкое дрожание частиц, размытие частиц, преобразование цвета и переход.Клавиатура поднята~
1. Процесс построения 3D-сцены
Вот несколько ключевых слов:场景
,灯光
,模型
,材质
,贴图与纹理
,相机
,渲染器
.
Затем я начал интерпретировать форму:
Бог сказал, устройте сцену! Итак, есть сцена,场景去纳这万事万物
.
Бог сказал, да будет свет! Значит, был свет,灯光去现这大千世界,否则一片漆黑
.
Богу немного не хватало гнева, поэтому он выдавил из грязи маленького человечка, а не Адама, ее звали Сяофан.
Бог смотрит налево и направо, вверх и вниз,这小芳果然生得俊俏,五官精致加长腿
, это называется模型
;
Хотя Сяофан не сделан из воды,却也在这晨光的照射下显得皮肤吹弹可破
, это называется材质
;
Бог необъяснимо застеснялся, махнул рукой и дал ему穿上一件花格子长裙,配上了乌黑的长发
, это называется贴图与纹理
;
Уголок рта Бога не поднялся, но сердце его было полно радости, Он молча наблюдал за своей работой,上帝视角仿佛定格在了这一瞬间
, глаз Бога相机
;
то, что видел Бог,由世界入眼之后大脑冥想计算所得
, этот умный и эффективный мозг渲染器
.
Следующие поздравления заранее, вы можете стать маленьким богом этого веб-мира 3D.
-
Общий процесс
class ThreeDWorld {
constructor(canvasContainer) {
// canvas容器
this.container = canvasContainer || document.body;
// 创建场景
this.createScene();
// 创建灯光
this.createLights();
// 性能监控插件
this.initStats();
// 物体添加
this.addObjs();
// 轨道控制插件(鼠标拖拽视角、缩放等)
this.orbitControls = new THREE.OrbitControls(this.camera);
this.orbitControls.autoRotate = true;
// 循环更新渲染场景
this.update();
}
}
-
Создать сцену
Нам нужно создать Three.js в процессе相机实例
, и установите相机位置
, положение прямой видимости;
затем создайте渲染器实例
, установите его尺寸
а также背景色
В то же время он также открыл его.阴影
Эффект будет более реалистичным при освещении, и его основная задача состоит в том, чтобы вычислить все изображения, видимые в данный момент, в своем собственном диапазоне размеров и нарисовать их на холсте;
Наконец, принимая во внимание возможное масштабирование экрана, отслеживайте изменения размера окна, чтобы динамически регулировать размер средства визуализации и соотношение сторон камеры для достижения наилучшего эффекта отображения.
createScene() {
this.HEIGHT = window.innerHeight;
this.WIDTH = window.innerWidth;
// 创建场景
this.scene = new THREE.Scene();
// 在场景中添加雾的效果,参数分别代表‘雾的颜色’、‘开始雾化的视线距离’、刚好雾化至看不见的视线距离’
this.scene.fog = new THREE.Fog(0x090918, 1, 600);
// 创建相机
let aspectRatio = this.WIDTH / this.HEIGHT;
let fieldOfView = 60;
let nearPlane = 1;
let farPlane = 10000;
/**
* PerspectiveCamera 透视相机
* @param fieldOfView 视角
* @param aspectRatio 纵横比
* @param nearPlane 近平面
* @param farPlane 远平面
*/
this.camera = new THREE.PerspectiveCamera(
fieldOfView,
aspectRatio,
nearPlane,
farPlane
);
// 设置相机的位置
this.camera.position.x = 0;
this.camera.position.z = 150;
this.camera.position.y = 0;
// 创建渲染器
this.renderer = new THREE.WebGLRenderer({
// 在 css 中设置背景色透明显示渐变色
alpha: true,
// 开启抗锯齿
antialias: true
});
// 渲染背景颜色同雾化的颜色
this.renderer.setClearColor(this.scene.fog.color);
// 定义渲染器的尺寸;在这里它会填满整个屏幕
this.renderer.setSize(this.WIDTH, this.HEIGHT);
// 打开渲染器的阴影地图
this.renderer.shadowMap.enabled = true;
// this.renderer.shadowMapSoft = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
// 在 HTML 创建的容器中添加渲染器的 DOM 元素
this.container.appendChild(this.renderer.domElement);
// 监听屏幕,缩放屏幕更新相机和渲染器的尺寸
window.addEventListener('resize', this.handleWindowResize.bind(this), false);
}
// 窗口大小变动时调用
handleWindowResize() {
// 更新渲染器的高度和宽度以及相机的纵横比
this.HEIGHT = window.innerHeight;
this.WIDTH = window.innerWidth;
this.renderer.setSize(this.WIDTH, this.HEIGHT);
this.camera.aspect = this.WIDTH / this.HEIGHT;
this.camera.updateProjectionMatrix();
}
Несколько моментов для объяснения:
Когда вам нужно имитировать визуальное явление, когда объекты постепенно размываются на расстоянии, вы можете включить эффект «тумана», при этом цвет тумана желательно должен быть таким же, как цвет сцены.
Есть два типа камер:正交投影相机(OrthographicCamera)
а также透视投影相机(PerspectiveCamera)
.
Посмотрите на первый,Ортогональная проекционная камера, конструктор выглядит следующим образом:
OrthographicCamera(left, right, top, bottom, near, far);
Площадь, ограниченная этими шестью проекционными поверхностями, является видимой областью, проецируемой камерой. Объекты, видимые камерой, не кажутся почти большими и очень маленькими. на самом деле далеко, рядом одинаковой длины (неудивительно, что кто-то использовал линейку для измерения ответа...), эффект следующий:
второй,перспективная проекционная камераЭто все больше в соответствии с человеческим глазом, чтобы увидеть рядом с гораздо меньшим в реальном мире, мы используем камеру.
Конструктор выглядит следующим образом:
/**
* PerspectiveCamera 透视相机
* @param fov 视角
* @param aspect 纵横比
* @param near 近平面
* @param far 远平面
*/
PerspectiveCamera(fov, aspect, near, far);
Куб, который он видит, выглядит так:
-
Создать свет
Здесь создаются 3 источника света:户外光源(HemisphereLight)
,环境光源(AmbientLight)
,DirectionalLight(平行光源)
.
наружный источник светаЕго можно использовать для имитации отражающего эффекта на открытом воздухе: чем ярче небо, тем темнее земля.
окружающий светЕго можно применить к любому углу объекта, и обычно он имеет почти белое мерцание, чтобы избежать полной темноты в определенной части объекта под определенным углом.
Параллельный источник светаэто то, что мы используемосновной источник света, который используется для создания теней, как если бы солнце светило параллельно земле.
createLights() {
// 户外光源
// 第一个参数是天空的颜色,第二个参数是地上的颜色,第三个参数是光源的强度
this.hemisphereLight = new THREE.HemisphereLight(0xaaaaaa, 0x000000, .9);
// 环境光源
this.ambientLight = new THREE.AmbientLight(0xdc8874, .2);
// 方向光是从一个特定的方向的照射
// 类似太阳,即所有光源是平行的
// 第一个参数是关系颜色,第二个参数是光源强度
this.shadowLight = new THREE.DirectionalLight(0xffffff, .9);
// 设置光源的位置方向
this.shadowLight.position.set(50, 50, 50);
// 开启光源投影
this.shadowLight.castShadow = true;
// 定义可见域的投射阴影
this.shadowLight.shadow.camera.left = -400;
this.shadowLight.shadow.camera.right = 400;
this.shadowLight.shadow.camera.top = 400;
this.shadowLight.shadow.camera.bottom = -400;
this.shadowLight.shadow.camera.near = 1;
this.shadowLight.shadow.camera.far = 1000;
// 定义阴影的分辨率;虽然分辨率越高越好,但是需要付出更加昂贵的代价维持高性能的表现。
this.shadowLight.shadow.mapSize.width = 2048;
this.shadowLight.shadow.mapSize.height = 2048;
// 为了使这些光源呈现效果,需要将它们添加到场景中
this.scene.add(this.hemisphereLight);
this.scene.add(this.shadowLight);
this.scene.add(this.ambientLight);
}
Что, если бы был только один источник света? Например только основной источник света (направленный источник света).
физика средней школы,看到的物体颜色是光线照射到物体表面,经过物体表面的吸收后反射回人眼的颜色
то, что мы обычно называем цветом объекта, означает, что он находится в太阳光
Зрительный цвет, который он представляет нам при освещении, и его зрительный цвет представляет собой смесь цветов, которые он не поглощает.
Немного вокруг, например, допустим, этот квадрат红色(0xff0000)
(на солнце), затем пучок白光(0xffffff)
(Солнечный свет) попал в него, оба取“与”(0xff0000 & 0xffffff)
получать0xff0000
(визуальный цвет), все еще красный.
Как показано на рисунке ниже, слева — красный квадрат, справа — белый квадрат, используя0xffffff
Параллельное облучение светом:
this.shadowLight = new THREE.DirectionalLight(0xffffff, 1.0);
// 物体添加
addObjs(){
// 红色方块
let cube = new THREE.BoxGeometry(20, 20, 20);
let mat = new THREE.MeshPhongMaterial({
color: new THREE.Color(0xff0000)
});
let m_cube = new THREE.Mesh(cube, mat);
m_cube.castShadow = true;
m_cube.position.x = -20;
// 白色方块
let cube2 = new THREE.BoxGeometry(20, 20, 20);
let mat2 = new THREE.MeshPhongMaterial({
color: new THREE.Color(0xffffff)
});
let m_cube2 = new THREE.Mesh(cube, mat2);
m_cube2.castShadow = true;
m_cube2.position.x = 20;
// 物体添加至场景
this.scene.add(m_cube);
this.scene.add(m_cube2);
}
Если заменить на0x00ffff
облучается параллельным источником света,0xff0000 & 0x00ffff
получать0x000000
(черный), поэтому исходный красный квадрат слева становится черным как смоль.
this.shadowLight = new THREE.DirectionalLight(0x00ffff, 1.0);
Видно, что только при одном источнике света сцена не только немного тусклая, но и когда установленный цвет источника света не белый, могут быть неожиданные цвета, которые мы можем найти, поэтому добавьте источники наружного света и источники окружающего света. Чтобы ослабить это Эффект поглощения цвета света также делает сцену ярче.Дополнительный эффект заключается в следующем (в это время основной источник света все еще0x00ffff
):
-
мониторинг производительности
Используя плагин stats.js для мониторинга производительности, вы можете визуально увидеть частоту кадров рендеринга.60FPS
Когда он выше, человеческий глаз будет чувствовать себя очень плавно, а производительность игры с курицей и видеокартой также должна поддерживать высокую частоту кадров и плавный игровой процесс, поэтому, когда вы обнаружите, что частота кадров 3D на веб-странице build значительно сокращается в сложных сценах, необходимо учитывать только оптимизацию производительности.
<!-- 在引入three.js库之后引入插件 -->
<script src="./lib/stats.min.js"></script>
initStats() {
this.stats = new Stats();
// 将性能监控屏区显示在左上角
this.stats.domElement.style.position = 'absolute';
this.stats.domElement.style.bottom = '0px';
this.stats.domElement.style.zIndex = 100;
this.container.appendChild(this.stats.domElement);
}
-
добавление объекта
Композицию 3D-объектов можно разделить на две части:
-
Геометрия: он используется для переноса всех компонентов, составляющих эту геометрию
顶点信息
так же как变换属性和方法
. Например, чтобы создать пустую геометрическую модель, вы можете увидеть, что основные свойства следующие:vertices
используется для сохранения информации о положении вершины; иfaceVertexUvs
Это многомерный массив, используемый для сохранения отношения УФ-сопоставления на модели, например, как текстура должна быть прикреплена к модели. У него также есть какие-то местные методы, которые поддерживают копирование моделейclone
, матричное преобразованиеapplyMatrix
, повернутьrotate
, увеличитьscale
с кастрюлейtranslate
и т.п.Конечно, Three.js имеет для нас встроенные геометрические модели самых разных форм, таких как盒子模型(BoxGeometry)
,圆形模型(CircleGeometry)
,球体模型(SphereGeometry)
,圆柱体模型(CylinderGeometry)
,平面模型(PlaneGeometry)
, все они задают правила расположения вершин и начальное UV-отображение.Для других геометрических моделей и подробного использования, пожалуйста, обратитесь к официальной документации.
-
Материалы: его можно просто описать как характеристики отражения объектов при освещении светом, и эти характеристики можно увидеть нашими человеческими глазами при обратной связи.
粗糙
,光滑
,透明
и другие визуальные явления.
Введение общего материала:
(1)基础网孔材料(MeshBasicMaterial)
: материал, который рисует геометрические фигуры в простой затененной (плоской или каркасной) форме.
Например, модель перегиба тора, нарисованная на плоскости, выглядит так:
(2)兰伯特网孔材料(MeshLambertMaterial)
: поверхность неэмиссильного материала (Ламберта), рассчитанная для каждой вершины; его можно понять как имеющее漫反射
Поверхностные свойства материала можно использовать для имитации эффекта негладкого шероховатого материала.
(3)Phong网孔材料(MeshPhongMaterial)
: для материалов с глянцевой поверхностью рассчитывается каждый пиксель, часто используется для имитации глянцевого эффекта металлов.
(4)着色器材料(ShaderMaterial)
: Материалы визуализируются с помощью пользовательских шейдеров.Шейдер — это программа, написанная на GLSL, которая может работать непосредственно на графическом процессоре., которые могут достигать эффектов, отличных от встроенных материалов. (Изображения нет) Это введено, потому что позже оно будет использоваться при реализации рендеринга частиц.
так! когда у нас есть模型
а также材质
После этого создайте сетку, т.е. нанесите на модель материал, и установите位置
положи это添加到场景
, вы можете получить полноценный 3D-объект!
Создание 3D-объектов
Сначала приходи к самой простой, создайте кубBoxGeometry
, конструктор которого:
BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
Первые три параметра представляют куб宽
,高
,长
, последние три параметра представляют соответствующее направление分段数量
; Что делает количество сегментов? Можно понять, что чем больше количество сегментов, тем мельче будет разделена геометрическая модель и тем больше будет количество вершин (например, когда количество сегментов достаточно велико для создания модели сферы, она будет достаточно круглой). ).
Пример сравнения кубов одного размера из разных сегментов вместе:
// 物体添加
addObjs(){
// 使用基础网孔材料
let mat = new THREE.MeshBasicMaterial({
color: 0xff0000,
// 绘制为线框
wireframe: true
});
// 创建立方体几何模型
let cube1 = new THREE.BoxGeometry(10, 20, 30, 1, 1, 1);
// 混合模型与材质
let m_cube1 = new THREE.Mesh(cube1, mat);
let cube2 = new THREE.BoxGeometry(10, 20, 30, 2, 2, 2);
let m_cube2 = new THREE.Mesh(cube2, mat);
let cube3 = new THREE.BoxGeometry(10, 20, 30, 3, 3, 3);
let m_cube3 = new THREE.Mesh(cube3, mat);
m_cube1.position.x = -30;
m_cube2.position.x = 0;
m_cube3.position.x = 30;
this.scene.add(m_cube1);
this.scene.add(m_cube2);
this.scene.add(m_cube3);
}
Используйте фактурный материал для создания «деревянного ящика»:
// 物体添加
addObjs(){
let cube = new THREE.BoxGeometry(20, 20, 20);
// 使用Phong网孔材料
let mat = new THREE.MeshPhongMaterial({
color: new THREE.Color(0xffffff),
// 导入纹理贴图
map: THREE.ImageUtils.loadTexture('img/crate.jpg')
});
let m_cube = new THREE.Mesh(cube, mat);
m_cube.castShadow = true;
this.scene.add(m_cube);
}
Когда модель куба использует текстуры, изображение будет неоднократно применяться к каждой грани на основе его собственного предустановленного UV-отображения, и если размер не совпадает, текстура будет автоматически растягиваться или сжиматься, чтобы соответствовать грани.Например, Изменить куб размер, чтобы стать следующим:
Конечно, мы можем назначить разные текстуры для каждой поверхности или изменить отношение UV-отображения, чтобы отобразить разные области карты на определенной поверхности.Если вам интересно, вы можетекликните сюда, см. принцип реализации.
-
рендеринг сцены
После стольких приготовлений, описанных выше, наконец пришло время визуализировать этот маленький трехмерный мир:
// 循环更新渲染
update() {
// 动画插件
TWEEN.update();
// 性能监测插件
this.stats.update();
// 渲染器执行渲染
this.renderer.render(this.scene, this.camera);
// 循环调用
requestAnimationFrame(() => {
this.update()
});
}
А вот иTWEEN
, который представляет собой обычный плагин для выполнения анимационных эффектов, может задавать процесс изменения значений свойств во времени, и его нужно вызывать при рендеринге каждого кадра.TWEEN.update()
Поддерживайте актуальную стоимость недвижимости.
По аналогии,stats
Плагин также вызывает егоupdate
функция, которая удобна для расчета информации о производительности, такой как частота кадров.
Самое главное, каждый кадр нужно вызывать渲染
Операция по обновлению текущей визуальной картины сцены.
Используйте последний шагrequestAnimationFrame
Неоднократно вызывая себя, чтобы достичь цели циклического рендеринга.requestAnimationFrame
Системный временной интервал используется для обеспечения наилучшей эффективности отрисовки.Он не приведет к чрезмерному рисованию и увеличению накладных расходов, поскольку интервал времени слишком короткий; это не приведет к зависанию анимации и не приведет к ее плавности, поскольку интервал времени слишком велик. , что является обязательным для домашнего путешествия.
Создание 3D-композиций
Мы можем воссоздать удовольствие от Lego, втиснув несколько разных объектов в один и тот же 3D-набор.
addObjs(){
let mat = new THREE.MeshPhongMaterial({
color: new THREE.Color(0xffffff);
});
// 创建一个3D物体组合容器
let group = new THREE.Object3D();
let radius = 40;
let m_cube;
for (let deg = 0; deg < 360; deg += 30) {
// 创建白色方块的mesh
m_cube = new THREE.Mesh(new THREE.BoxGeometry(20, 20, 20), mat);
// 设置它可以产生投影
m_cube.castShadow = true;
// 设置它可以接收其他物体在其表面的投影
m_cube.receiveShadow = true;
// 用方块画个圈
m_cube.position.x = radius * Math.cos(Math.PI * deg / 180);
m_cube.position.y = radius * Math.sin(Math.PI * deg / 180);
// z轴位置错落摆放
m_cube.position.z = deg % 60 ? 5 : -5;
// 放入容器
group.add(m_cube);
}
// 3D组合添加至场景
this.scene.add(group);
}
2. Импорт внешних моделей
Когда сложные модели должны быть представлены на веб-страницах, модели необходимо импортировать извне.Различное программное обеспечение для создания моделей, такое как3dmax
а такжеBlender
, они могут экспортировать 3D-файлы в разные форматы; на рынке так много 3D-форматов, извините за мое невежество, я не слышал о большинстве из них..., но Three.js предоставляет более полный плагин для импорта моделей , которые могуткликните сюдаПроверить.
Вот три 3D-формата, которые, как мне кажется, обычно используются (не смейте быть настойчивым, просто мое мнение):
-
js/json
Фронтендер это очень любезно увидел, это 3D формат, специально разработанный для Three.js, а загрузчик тоже включен в библиотеку Three.js. -
obj与mtl
OBJ — это простой формат файла 3D, используемый только для определения геометрии объекта. Файлы MTL часто используются вместе с файлами OBJ.В файле MTL определяется материал объекта. -
fbx
Это формат, используемый программным обеспечением FilmBoX. Его наибольшее использование для взаимного управления моделью, материалом, действием и информацией о камере между программами, такими как max, maya, softimage и т. д., поэтому он используется для создания 3D-контента между приложениями. С непревзойденной функциональной совместимостью.
а такжеvtk
формат сply
Формат не будет представлен первым. (Потому что у меня нет этих моделей под рукой...(´-ι_-`)
)
Не забудьте установить соответствующие плагины перед использованием:
<script src="./lib/OBJLoader.js"></script>
<script src="./lib/MTLLoader.js"></script>
<script src="./lib/inflate.min.js"></script>
<script src="./lib/FBXLoader.js"></script>
который появилсяinflate.min.js
,ДаFBXLoader
Плагины, необходимые для использования.
Напишите свой собственныйloader
, который может одновременно загружать несколько моделей в разных форматах:
// 自定义模型加载器
loader(pathArr) {
// 各类loader实例
let jsonLoader = new THREE.JSONLoader();
let fbxLoader = new THREE.FBXLoader();
let mtlLoader = new THREE.MTLLoader();
let objLoader = new THREE.OBJLoader();
let basePath, pathName, pathFomat;
if (Object.prototype.toString.call(pathArr) !== '[object Array]') {
pathArr = new Array(1).fill(pathArr.toString());
}
let promiseArr = pathArr.map((path) => {
// 模型基础路径
basePath = path.substring(0, path.lastIndexOf('/') + 1);
// 模型名称
pathName = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.'));
// 后缀为js或json的文件统一当做js格式处理
pathName = pathName === 'json' ? 'js' : pathName;
// 模型格式
pathFomat = path.substring(path.lastIndexOf('.') + 1).toLowerCase();
switch (pathFomat) {
case 'js':
return new Promise(function(resolve) {
jsonLoader.load(path, (geometry, material) => {
resolve({
// 对于js文件,加载到的模型与材质分开放置
geometry: geometry,
material: material
})
});
});
break;
case 'fbx':
return new Promise(function(resolve) {
fbxLoader.load(path, (object) => {
resolve(object);
});
});
break;
case 'obj':
return new Promise(function(resolve) {
objLoader.load(path, (object) => {
resolve(object);
});
});
break;
case 'mtl':
return new Promise(function(resolve) {
mtlLoader.setPath(basePath);
mtlLoader.load(pathName + '.mtl', (mtl) => {
resolve(mtl);
});
});
break;
case 'objmtl':
return new Promise(function(resolve, reject) {
mtlLoader.setPath(basePath);
mtlLoader.load(`${pathName}.mtl`, (mtl) => {
mtl.preload();
objLoader.setMaterials(mtl);
objLoader.setPath(basePath);
objLoader.load(pathName + '.obj', resolve, undefined, reject);
});
});
break;
default:
return '';
}
});
return Promise.all(promiseArr);
}
В дополнение к вышеперечисленному загружается отдельноjs/json
,fbx
,obj
,mtl
В дополнение к файлу, пользовательскийobjmtl
, удобно поставить одно и то же имяobj
а такжеmtl
Возврат после создания сетки файла. Следует отметить, что приведенный здесь список представляет собой только импорт статических моделей, таких как некоторые 3D-форматы, которые могут содержать анимационную информацию, такую какjs/json
,fbx
, Для выполнения его анимационных эффектов требуется дополнительная обработка. В примере на официальном сайте Three.js подробно описаны методы использования различных загрузчиков и их коды выполнения анимации. Вы можете обращаться к ним при необходимости.
Под тестом:
addObjs() {
this.loader(['obj/bumblebee/bumblebee.FBX', 'obj/teapot.js', 'obj/monu9.objmtl']).then((result) => {
let bumblebee = result[0];
// 加载的js/json格式需手动mesh
let teapot = new THREE.Mesh(result[1].geometry, result[1].material);
let monu = result[2];
// 按场景要求缩放及位移
bumblebee.scale.x = 0.03;
bumblebee.scale.y = 0.03;
bumblebee.scale.z = 0.03;
bumblebee.rotateX(-Math.PI / 2);
bumblebee.position.y -= 30;
teapot.applyMatrix(new THREE.Matrix4().makeTranslation(0, -30, 20));
teapot.scale.x = 0.2;
teapot.scale.y = 0.2;
teapot.scale.z = 0.2;
monu.applyMatrix(new THREE.Matrix4().makeTranslation(0, -30, 0));
// 开启投影
this.onShadow(monu);
this.onShadow(bumblebee);
this.onShadow(teapot);
// 添加至场景
this.scene.add(bumblebee);
this.scene.add(teapot);
this.scene.add(monu);
});
}
// 递归遍历模型及模型子元素并开启投影
onShadow(obj) {
if (obj.type === 'Mesh') {
obj.castShadow = true;
obj.receiveShadow = true;
}
if (obj.children && obj.children.length > 0) {
obj.children.forEach((item) => {
this.onShadow(item);
})
}
return;
}
так получилось大黄蜂
существует纪念碑谷
На карте Вэй Ран стояла, но смотрела на необъяснимую фигуру, появившуюся на земле.小茶壶
сцена ).
Если вы все еще чувствуетеjs/json
Этот формат приятен в использовании, и я хочу конвертировать другие 3D-форматы вjs/json
, за исключением использования программного обеспечения, такого какBlender
Экспорт вне конверсии, есть небольшой онлайн-инструмент может сделать это -convert_to_threejs.py
3. Мышь взаимодействует с объектами сцены
-
контроль трека
Помните в самом началеconstructor函数里
созданныйorbitControls
плагин? После того, как он введен и включен, мышь может управлять сценой.旋转
,缩放
а также位移
, это только видимость, на самом деле он манипулирует相机位置
, при масштабировании камера увеличивается и уменьшается, а при вращении положение камеры вращается вокруг центра сцены.При смещении требуются дополнительные вычисления для регулировки положения камеры, чтобы сцена выглядела панорамируемой.
Управление также имеет точно настроенные параметры, такие как阻尼系数
,控制旋转或缩放范围
И так далее, вы можете найти его.
<script src="./lib/OrbitControls.js"></script>
// 轨道控制插件
this.orbitControls = new THREE.OrbitControls(this.camera);
this.orbitControls.autoRotate = true;
-
Щелчок мышью и взаимодействие при наведении
Поскольку объекты в трехмерном мире не могут быть напрямую связаны с такими событиями, как «щелчок», как DOM веб-страницы, когда мышь щелкает по экрану, как определить, был ли щелкнут объект внутри?
Three.js предоставляет射线(Raycaster)
Класс, который может испускать лучи из одной точки в другую, возвращая объект или даже расстояние, которое проходит луч.
Таким образом, использование мыши для захвата объектов можно описать следующим образом:
(1) Получить щелчок мыши屏幕坐标点
;
(2) В соответствии с размером окна холста и точками координат экрана вычислите точку, сопоставленную с 3D-сценой.场景坐标点
;
(3) по视线位置
нажать场景坐标点
испускать излучение;
(4) Чтобы получить возвращаемую информацию луча, луч собирает соответствующую информацию в порядке прохождения через объекты и помещает ее в массив, обычно от первого элемента массива до выбранного объекта.
Простой щелчок мышью для получения кода объекта выглядит следующим образом:
this.container.addEventListener("mousedown", (event) => {
let mouse = new THREE.Vector2();
let raycaster = new THREE.Raycaster();
// 计算鼠标点击位置转换到3D场景后的位置
mouse.x = (event.clientX / this.renderer.domElement.clientWidth) * 2 - 1;
mouse.y = -(event.clientY / this.renderer.domElement.clientHeight) * 2 + 1;
// 由当前相机(视线位置)像点击位置发射线
raycaster.setFromCamera(mouse, this.camera);
let intersects = raycaster.intersectObjects(this.scene.children, true)
if (intersects.length > 0) {
// 拿到射线第一个照射到的物体
console.log(intersects[0].object);
}
});
Однако, как человек, работающий с интерфейсом, я по-прежнему думаю, что было бы здорово, если бы я мог привязывать события щелчка к объектам, таким как обычные веб-страницы.
Итак, я инкапсулировал что-то вроде этого (только для справки):
addMouseListener() {
// 层层往上寻找模型的父级,直至它是场景下的直接子元素
function parentUtilScene(obj) {
if (obj.parent.type === 'Scene') return obj;
while (obj.parent && obj.parent.type !== 'Scene') {
obj = obj.parent;
}
return obj;
}
// canvas容器内鼠标点击事件添加
this.container.addEventListener("mousedown", (event) => {
this.handleRaycasters(event, (objTarget) => {
// 寻找其对应父级为场景下的直接子元素
let object = parentUtilScene(objTarget);
// 调用拾取到的物体的点击事件
object._click && object._click(event);
// 遍历场景中除当前拾取外的其他物体,执行其未被点击到的事件回调
this.scene.children.forEach((objItem) => {
if (objItem !== object) {
objItem._clickBack && objItem._clickBack();
}
});
});
});
// canvas容器内鼠标移动事件添加
this.container.addEventListener("mousemove", (event) => {
this.handleRaycasters(event, (objTarget) => {
// 寻找其对应父级为场景下的直接子元素
let object = parentUtilScene(objTarget);
// 鼠标移动到拾取物体上且未离开时时,仅调用一次其悬浮事件方法
!object._hover_enter && object._hover && object._hover(event);
object._hover_enter = true;
// 遍历场景中除当前拾取外的其他物体,执行其未有鼠标悬浮的事件回调
this.scene.children.forEach((objItem) => {
if (objItem !== object) {
objItem._hover_enter && objItem._hoverBack && objItem._hoverBack();
objItem._hover_enter = false;
}
});
})
});
// 为所有3D物体添加上“on”方法,可监听物体的“click”、“hover”事件
THREE.Object3D.prototype.on = function(eventName, touchCallback, notTouchCallback) {
switch (eventName) {
case "click":
this._click = touchCallback ? touchCallback : undefined;
this._clickBack = notTouchCallback ? notTouchCallback : undefined;
break;
case "hover":
this._hover = touchCallback ? touchCallback : undefined;
this._hoverBack = notTouchCallback ? notTouchCallback : undefined;
break;
default:;
}
}
}
// 射线处理
handleRaycasters(event, callback) {
let mouse = new THREE.Vector2();
let raycaster = new THREE.Raycaster();
mouse.x = (event.clientX / this.renderer.domElement.clientWidth) * 2 - 1;
mouse.y = -(event.clientY / this.renderer.domElement.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, this.camera);
let intersects = raycaster.intersectObjects(this.scene.children, true)
if (intersects.length > 0) {
callback && callback(intersects[0].object);
}
}
Несмотря на максимально подробные комментарии, мне пришлось вскочить и объяснить, что делает приведенный выше код.
Когда мышь перемещается по экрану, весь контейнер холста привязывается.mousedown
,mouseup
,mousemove
мероприятие(touch
события класса) будет связующим звеном между взаимодействием между мышью и 3D-сценой.
Итак, я в базовом классе 3D-объектов.THREE.Object3D
добавил "on
” метод прототипа, в настоящее время поддерживает только пользовательскийclick
а такжеhover
событие, при вызове которого метод-прототип только монтирует соответствующий обработчик обратного вызова на 3D-объекте;
например привязкаclick
событие, останется два параметраtouchCallback
,notTouchCallback
Назначено 3D-объекту_click
а также_clickBack
В атрибуте метода подтаблица представляет点击到该物体的回调函数
а также点击了却没有点击到自己时候的回调函数
(Последний обратный вызов кажется бесполезным, но в некоторых случаях он работает хорошо, если он вам не нужен, просто не передавайте его);
Затем привяжите родной для всего контейнера холстаmousedown
а такжеmousemove
мероприятие;
mousedown
При срабатывании строка запуска получает первый объект, по которому щелкают, чтобы увидеть, есть ли на нем какой-либо объект._click
метод, выполнить, если он есть; затем пройти через непосредственные дочерние элементы, кроме самого себя в сцене (то естьscene.add
метод, добавленный в сцену), посмотрите, есть ли у них какие-либо_clickBack
метод, выполните его.
mousemove
используется для моделирования объектовhover
Событие, когда мышь движется по экрану, используйте луч, чтобы объект коснулся мышью, как и раньше, и выполните его_hover
метод, другие объекты в сцене выполняют свои_hoverBack
метод, стоит отметить, что я добавил еще один_hover_enter
Переменная флага для определения состояния мыши в текущем объекте:鼠标已在物体之上
еще鼠标已离开物体
, чтобы избежать повторного выполнения функции обратного вызова.
но! Когда вы нажимаете на объект, думаете ли вы, что ваш эксперт по лучам может помочь вам получить тело объекта, который вы видите! !!!∑(゚Д゚ノ)ノ
При использовании сложных внешних моделей импорт может привести к物体组合(Group)
, внутри негоchildren
Собраны вместе, чтобы сформировать внешний вид корпуса; например, импортные大黄蜂模型
, вы хотите понять, что он может вращаться на месте на 360 градусов, когда вы нажимаете на него, поэтому вы отправляете луч и уверенно трансформируете первый возвращенный объект.В результате вы можете обнаружить, что,只是它的一只胳膊开始跳舞
, или空气突然安静
.
Для решения вышеуказанных проблем, которые могут существовать, добавлен приведенный выше код.parentUtilScene
Функция используется для нахождения родительского контейнера объекта слой за слоем, пока он не станет прямым дочерним элементом в сцене, так что получается полное тело модели.
Пример добавления взаимодействия с указателем и мышью к импортированной модели выше:
addObjs(){
this.loader(['obj/bumblebee/bumblebee.FBX', 'obj/teapot.js', 'obj/monu9.objmtl']).then((result) => {
let bumblebee = result[0];
let teapot = new THREE.Mesh(result[1].geometry, result[1].material);
let monu = result[2];
bumblebee.scale.x = 0.03;
bumblebee.scale.y = 0.03;
bumblebee.scale.z = 0.03;
bumblebee.rotateX(-Math.PI / 2);
bumblebee.position.y -= 30;
teapot.applyMatrix(new THREE.Matrix4().makeTranslation(0, -30, 20));
teapot.scale.x = 0.2;
teapot.scale.y = 0.2;
teapot.scale.z = 0.2;
monu.applyMatrix(new THREE.Matrix4().makeTranslation(0, -30, 0));
this.onShadow(monu);
this.onShadow(bumblebee);
this.onShadow(teapot);
// 大黄蜂模型被点击时向z轴移动一段距离
bumblebee.on("click", function() {
let tween = new TWEEN.Tween(this.position).to({
z: -10
}, 200).easing(TWEEN.Easing.Quadratic.InOut).start();
});
// 茶壶模型在鼠标悬浮时放大,鼠标离开时缩小
teapot.on("hover", function() {
let tween = new TWEEN.Tween(this.scale).to({
x: 0.3,
y: 0.3,
z: 0.3
}, 200).easing(TWEEN.Easing.Quadratic.InOut).start();
}, function() {
let tween = new TWEEN.Tween(this.scale).to({
x: 0.2,
y: 0.2,
z: 0.2
}, 200).easing(TWEEN.Easing.Quadratic.InOut).start();
});
this.scene.add(bumblebee);
this.scene.add(teapot);
this.scene.add(monu);
});
}
В-четвертых, трехмерное построение системы частиц.
Чтобы примерно объяснить основные ценности Three.js, и продвигать дух социализма, я за всех и делюсь знаниями, я только начал осознавать эффект частиц в начале статьи... Боль и насморк ヾ(༎ຶД༎ຶ)ノ"
Нет, я просто был очарован ветром и песком.
кашель~
Кусок простейшего кода системы частиц:
// 创建球体模型
let ball = new THREE.SphereGeometry(40, 30, 30);
// 创建粒子材料
let pMaterial = new THREE.PointsMaterial({
// 粒子颜色
color: 0xffffff,
// 粒子大小
size: 2
});
// 创建粒子系统
let particleSystem = new THREE.ParticleSystem(ball, pMaterial);
// 加入场景
this.scene.add(particleSystem);
Система частиц читается в моделиvertices
Атрибуты, то есть все позиции вершин, комбинируются с материалами частиц для создания эффектов частиц.Эффект приведенного выше кода следующий: можно заметить, что частицы будут отображаться в форме блока по умолчанию.Если вы хотите изменить его, вы можете передать его при создании материала частиц.map
Атрибут, применить изображение или применить результат рисования холста, о котором будет сказано позже.
Пять, преобразование частиц
Превращение частиц в конечном счете состоит в том, что частицы位置
,颜色
,尺寸
Преобразование, метод рендеринга можно разделить на разные сценарии расчета.CPU渲染
а такжеGPU渲染
.
Для грубого рендеринга частиц,
Состояние всех частиц сохраняется в js-коде для расчета, принадлежащемCPU渲染
.
Поддерживать информацию о состоянии частиц вshader(着色器)代码
рассчитывается вGPU渲染
.
Когда нам нужно просто изменить положение каждой частицы, производительность рендеринга с использованием ЦП в порядке и проста для понимания.Если все частицы изменяются в нескольких состояниях одновременно, рендеринг с использованием графического процессора будет более плавным.
Здесь будет использоваться метод рендеринга GPU, то есть нужно написать шейдерную программу.
Тот же простой эффект частиц, что и раньше, код после использования самого простого вмешательства в шейдер выглядит следующим образом:
<!-- html中加入shader代码 -->
<!-- 顶点着色器代码 -->
<script type="x-shader/x-vertex" id="vertexshader">
void main() {
gl_PointSize = 4.;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<!-- 片元着色器代码 -->
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
</script>
addObjs(){
// 加载星球大战里那个叫“BB-8”的机器人模型
this.loader(['obj/robot.fbx']).then((result) => {
// 提取出其几何模型
let robotObj = result[0].children[1].geometry;
// 适当变换使其完整在屏幕显示
robotObj.scale(0.08, 0.08, 0.08);
robotObj.rotateX(-Math.PI / 2);
robotObj.applyMatrix(new THREE.Matrix4().makeTranslation(0, 10, 0));
// 把它变成粒子
this.addPartice(robotObj);
});
}
// 将几何模型变成几何缓存模型
toBufferGeometry(geometry) {
if (geometry.type === 'BufferGeometry') return geometry;
return new THREE.BufferGeometry().fromGeometry(geometry);
}
// 模型转化成粒子
addPartice(obj) {
obj = this.toBufferGeometry(obj);
// 传递给shader的属性
let uniforms = {
// 传递的颜色属性
color: {
type: 'v3', // 指定变量类型为三维向量
value: new THREE.Color(0xffffff)
}
};
// 创建着色器材料
let shaderMaterial = new THREE.ShaderMaterial({
// 传递给shader的属性
uniforms: uniforms,
// 获取顶点着色器代码
vertexShader: document.getElementById('vertexshader').textContent,
// 获取片元着色器代码
fragmentShader: document.getElementById('fragmentshader').textContent,
// 渲染粒子时的融合模式
blending: THREE.AdditiveBlending,
// 关闭深度测试
depthTest: false,
// 开启透明度
transparent: true
});
let particleSystem = new THREE.Points(obj, shaderMaterial);
this.scene.add(particleSystem);
}
Эффект следующий:
какие! Разве это не тот же эффект, что и в предыдущем абзаце, всего в несколько строк кода? Зачем писать так много!
Но на этом основании измените только одну строку кода:
<script type="x-shader/x-vertex" id="vertexshader">
void main() {
// 这是被修改的那一行
gl_PointSize = 4. + 2. * sin(position.y / 4.);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
</script>
Эффекта периодического изменения размера частиц по оси Y можно добиться:
Теперь моя очередь начинать притворяться, что объясняю все тонкости:
1. 着色器(shader)
что это такое?
(Глядя на название, кажется, что оно используется для окраски)
Шейдер — это последний шаг перед рендерингом экрана на экране, его можно использовать для изменения результатов предыдущего рендеринга, включая изменение такой информации, как цвет, положение и т. д., и даже для постобработки результатов рендеринга. предыдущий рендеринг для достижения расширенных эффектов рендеринга.
Обычно используемые шейдеры делятся на顶点着色器(Vertex Shader)
а также片元着色器(Fragment Shader)
.
顶点着色器
: каждый顶点
Программа, вызываемая один раз, в которой можно получить доступ к вершинам位置
,颜色
,法向量
и другую информацию, выполнить над ними соответствующие вычисления для достижения конкретных эффектов рендеринга или передать эти значения фрагментному шейдеру.
片元着色器
: каждый片元
Однократно вызываемая программа, в которой можно получить доступ к координатам, информации о глубине, цвете и другой информации фрагмента на двухмерном экране. Изменяя эти значения, можно добиться определенных эффектов рендеринга.
2. shader
Что является основным в программе?
shader程序
это C-подобный язык,void main(){}
Это точка входа в программу, перед этим нужно объявить используемые переменные, напримерuniform vec3 color;
, Представляя变量传递类型 变量类型 变量名;
("变量传递类型
"Это имя мое собственное, понятное)
变量传递类型
Есть три вида:
-
attribute
: используется для получения данных вершин, передаваемых ЦП, обычно используется для получения данных, передаваемых из кода js.顶点坐标
,法线
,纹理坐标
,顶点颜色
и другая информация,attribute 只能在顶点着色器中被声明与使用
. -
uniform
: может совместно использоваться вершинными и фрагментными шейдерами, и метод объявления тот же, обычно используемый для объявления变换矩阵
,材质
,光照参数
а также颜色
и другая информация. -
varying
: Используется для передачи данных между вершинным и фрагментным шейдерами. Как правило, вершинный шейдер изменяет значение переменной переменной, а затем фрагментный шейдер использует значение переменной переменной. Таким образом, объявление различных переменных между вершинным и фрагментным шейдерами должно быть一致的
.
变量类型
Есть следующие:
-
void
: Как и void в языке C, у него нет типа. -
bool
: Логический тип. -
int
: Целое число со знаком. -
float
число с плавающей запятой. -
vec2
,vec3
,vec4
: 2-х, 3-х и 4-х мерные векторы, которые также можно понимать как массивы 2-х, 3-х и 4-х длин. -
bvec2
,bvec3
,bvec4
: Вектор логических значений 2, 3 и 4 измерений. -
ivec2
,ivec3
,ivec4
: 2, 3, 4-мерный вектор значений int. -
mat2
,mat3
,mat4
: матрица поплавков 2x2, 3x3, 4x4. -
sampler2D
: Текстура. -
samplerCube
: Текстура куба.
Поскольку это C-подобный язык, он не выполняет автоматическое и неявное преобразование типов переменных, таких как js.Поэтому переменные должны быть строго объявлены перед использованием, а числа одного и того же типа можно только складывать, вычитать, умножать и делить во время числовых операций. . Например1 + 1.0
сообщит об ошибке.
变量精度
:
Используйте их для добавления типов переменных (например,varying highp float my_number
)
-
highp
: 16 бит, диапазон с плавающей запятой (-2 ^ 62, 2 ^ 62), диапазон целых чисел (-2 ^ 16, 2 ^ 16) -
mediump
: 10 бит, диапазон с плавающей запятой (-2 ^ 14, 2 ^ 14), диапазон целых чисел (-2 ^ 10, 2 ^ 10) -
lowp
: 8 бит, диапазон с плавающей запятой (-2, 2), диапазон целых чисел (-2 ^ 8, 2 ^ 8)
Если вы хотите, чтобы все числа с плавающей запятой были высокоточными, вы можете объявить в верхней части шейдераprecision highp float;
, что устраняет необходимость объявлять точность для каждой переменной.
shader中向量的访问:
когда у нас естьvec4
Когда четырехмерный вектор :
(Это гибкое значение меня тоже удивило)
- vec4.x, vec4.y, vec4.z, vec4.w можно убрать по четырем значениям x, y, z, w соответственно.
- Любая комбинация vec4.xy, vec4.xx, vec4.xz, vec4.xyz может извлекать векторы нескольких измерений.
- vec4.r, vec4.g, vec4.b, vec4.a также могут выносить 4 значения через r, g, b и a, которые можно произвольно комбинировать, как указано выше.
- vec4.s, vec4.t, vec4.p, vec4.q также могут выносить 4 значения через s, t, p, q, которые можно произвольно комбинировать, как указано выше.
- vec3 и vec2 похожи, но переменные относительно уменьшены.Например, vec3 имеет только x, y, z, а vec2 имеет только x, y.
shader内置变量
:
-
gl_Position
: HERTEX SHIPER, написание положения вершины; примитивно собирается, резка и другие функции с использованием операции фиксации; оператор в нем:highp vec4 gl_Position;
-
gl_PointSize
: для вершинного шейдера укажите размер точки и количество пикселей после растеризации; его внутреннее объявление:mediump float gl_Position;
-
gl_FragColor
: используется для фрагментного шейдера, записывает цвет фрагмента, используется последующими фиксированными конвейерами;mediump vec4 gl_FragColor;
-
gl_FragData
: для фрагментного шейдера это массив, запишите gl_FragData[n] как данные n, используемые последующими фиксированными конвейерами;mediump vec4 gl_FragData[gl_MaxDrawBuffers];
gl_FragColor и gl_FragData являются взаимоисключающими и не будут записываться одновременно; -
gl_FragCoord
: Для шейдера фрагментов, только для чтения, координата положения фрагмента относительно окна x, y, z, 1/w, генерируется после исправления разницы между примитивами конвейера, z — значение глубины;mediump vec4 gl_FragCoord;
-
gl_FrontFacing
: используется для определения того, принадлежит ли фрагмент фронтальному примитиву, только для чтения;bool gl_FrontFacing;
-
gl_PointCoord
: только для точечных примитивов,mediump vec2 gl_PointCoord;
shader内置函数:
3. shader
Как ты пишешь?
Давайте взглянем на базовый код вершинного шейдера:
(Причина, по которой он завернут в примечание к скрипту, заключается в том, чтобы упростить для three.js получение его текстового значения)
<!-- 顶点着色器-->
<script type="x-shader/x-vertex" id="vertexshader">
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
vertex shader
необходимо указать вgl_Position
значение, потому что самая важная задача вершинного шейдера — вычислить значение вершины на экране真实位置
, если это значение не вычислено, вершинный шейдер недействителен.
Затем он внезапно появился без заявления позже.projectionMatrix
,modelViewMatrix
,position
Что за черт?
Посмотрите на имена, они投影矩阵
,模型视图矩阵
,位置
; они автоматически вычисляются при запуске вершинного шейдера. так почемуgl_Position
Вы хотите рассчитать?
position
Ну, это относится к трехмерному вектору, представляющему положение нижней вершины, которое является xyz вершины в модели, но только зная это значение, рендереру недостаточно нарисовать, где она находится, потому что экран компьютера является двухмерным, и положение камеры также может меняться, а размер окна, используемого для отображения, не является фиксированным...
Поэтому вам нужно использовать矩阵变换
!
Во-первых, схематическая диаграмма картины Великого Бога:
Продиктуйте следующий процесс: трехмерный вектор координат объекта, умноженный на模型视图矩阵
после того, как смог получить его в视图坐标系
Положение в , то есть его координатное положение относительно камеры, то умножить на投影矩阵
, задайте положение каждой точки на двумерной плоскости и получите ее в投影坐标系
положение в ; умножается на视口矩阵
, получить его в屏幕坐标系
Положение посередине, то есть то, что мы видим, когда сидим перед компьютером.
Таким образом, можно получить формулу преобразования:
Поскольку умножение матриц занимает много времени, а视图矩阵
а также模型矩阵
Обычно он неизменен, поэтому по матричному ассоциативному закону произведение их может быть вычислено и закешировано первым, что равноmodelViewMatrix
,а также模型点坐标
из оригинала三维向量(position)
был расширен до四维向量(vec4(position, 1.0))
, потому что другие матрицы на самом деле имеют размер 4*4, и их нельзя перемножить без расширения координат точки до четырех измерений;
Снова вопрос, зачем нужно создавать четырехмерную матрицу в трехмерном мире?
Простое объяснение, трехмерная матрица может реализовать положение旋转
а также缩放
, но если вы хотите перевести, вам нужно добавить齐次的一维
, более подробное объяснение см.эта статья.
Выяснив это, мы хотим использовать вершинный шейдер для динамического изменения положения точек модели, и мы можем публиковать статьи в любом месте процесса умножения.
Теперь основной код вершинного шейдера:
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
</script>
片元着色器
Конечная задача - это рассчитать顶点颜色
, поэтому он должен быть задан в этой программеgl_FragColor
Значение , которое, как и координаты положения, также является четырехмерным вектором Первые три измерения можно понимать какrbg
Значение цвета , но диапазон (0,0, 1,0), четвертое измерение представляет颜色透明度
. Начальный цвет общей вершины может быть передан ЦПuniform
Передайте его, а затем отдайте после причудливого расчета.gl_FragColor
.
Кроме того, очень интересно, что значение этого вектора очень интересное и очень удовлетворительное;
gl_FragColor = vec4(color, 1.0);
преобразовать 3D-векторcolor
Он автоматически расширяется до первых трех цифр четырехмерного вектора;
Если вы пишете:gl_FragColor = vec4(1.0, color);
, то значение синего канала в переданном значении цвета действует на прозрачность.
Конечно, самый честный:gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
, ум чист и бел, без отвлекающих мыслей.
Шесть, шейдерные эффекты частиц
Столько предзнаменований! Наконец-то это работает! Это работает!
1. Преобразование смещения частиц
Сбой в коде:
addObjs() {
// 加载了两个模型,用于粒子变换
this.loader(['obj/robot.fbx', 'obj/Guitar/Guitar.fbx']).then((result) => {
let robot = result[0].children[1].geometry;
let guitarObj = result[1].children[0].geometry;
guitarObj.scale(1.5, 1.5, 1.5);
guitarObj.rotateX(-Math.PI / 2);
robot.scale(0.08, 0.08, 0.08);
robot.rotateX(-Math.PI / 2);
this.addPartices(robot, guitarObj);
});
}
// 几何模型转缓存几何模型
toBufferGeometry(geometry) {
if (geometry.type === 'BufferGeometry') return geometry;
return new THREE.BufferGeometry().fromGeometry(geometry);
}
// 粒子变换
addPartices(obj1, obj2) {
obj1 = this.toBufferGeometry(obj1);
obj2 = this.toBufferGeometry(obj2);
let moreObj = obj1
let lessObj = obj2;
// 找到顶点数量较多的模型
if (obj2.attributes.position.array.length > obj1.attributes.position.array.length) {
[moreObj, lessObj] = [lessObj, moreObj];
}
let morePos = moreObj.attributes.position.array;
let lessPos = lessObj.attributes.position.array;
let moreLen = morePos.length;
let lessLen = lessPos.length;
// 根据最大的顶点数开辟数组空间,同于存放顶点较少的模型顶点数据
let position2 = new Float32Array(moreLen);
// 先把顶点较少的模型顶点坐标放进数组
position2.set(lessPos);
// 剩余空间重复赋值
for (let i = lessLen, j = 0; i < moreLen; i++, j++) {
j %= lessLen;
position2[i] = lessPos[j];
position2[i + 1] = lessPos[j + 1];
position2[i + 2] = lessPos[j + 2];
}
// sizes用来控制每个顶点的尺寸,初始为4
let sizes = new Float32Array(moreLen);
for (let i = 0; i < moreLen; i++) {
sizes[i] = 4;
}
// 挂载属性值
moreObj.addAttribute('size', new THREE.BufferAttribute(sizes, 1));
moreObj.addAttribute('position2', new THREE.BufferAttribute(position2, 3));
// 传递给shader共享的的属性值
let uniforms = {
// 顶点颜色
color: {
type: 'v3',
value: new THREE.Color(0xffffff)
},
// 传递顶点贴图
texture: {
value: this.getTexture()
},
// 传递val值,用于shader计算顶点位置
val: {
value: 1.0
}
};
// 着色器材料
let shaderMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
blending: THREE.AdditiveBlending,
depthTest: false,// 这个不设置的话,会导致带透明色的贴图始终会有方块般的黑色背景
transparent: true
});
// 创建粒子系统
let particleSystem = new THREE.Points(moreObj, shaderMaterial);
let pos = {
val: 1
};
// 使val值从0到1,1到0循环往复变化
let tween = new TWEEN.Tween(pos).to({
val: 0
}, 1500).easing(TWEEN.Easing.Quadratic.InOut).delay(1000).onUpdate(callback);
let tweenBack = new TWEEN.Tween(pos).to({
val: 1
}, 1500).easing(TWEEN.Easing.Quadratic.InOut).delay(1000).onUpdate(callback);
tween.chain(tweenBack);
tweenBack.chain(tween);
tween.start();
// 每次都将更新的val值赋值给uniforms,让其传递给shader
function callback() {
particleSystem.material.uniforms.val.value = this.val;
}
// 粒子系统添加至场景
this.scene.add(particleSystem);
this.particleSystem = particleSystem;
}
// 用canvas画了个带渐变的圆,将该图像作为纹理返回
getTexture(canvasSize = 64) {
let canvas = document.createElement('canvas');
canvas.width = canvasSize;
canvas.height = canvasSize;
canvas.style.background = "transparent";
let context = canvas.getContext('2d');
let gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, canvas.width / 8, canvas.width / 2, canvas.height / 2, canvas.width / 2);
gradient.addColorStop(0, '#fff');
gradient.addColorStop(1, 'transparent');
context.fillStyle = gradient;
context.beginPath();
context.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2, true);
context.fill();
let texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;
}
Код шейдера выглядит следующим образом:
<script type="x-shader/x-vertex" id="vertexshader">
attribute float size;
attribute vec3 position2;
uniform float val;
void main() {
vec3 vPos;
// 变动的val值引导顶点位置的迁移
vPos.x = position.x * val + position2.x * (1. - val);
vPos.y = position.y * val + position2.y * (1. - val);
vPos.z = position.z * val + position2.z * (1. - val);
vec4 mvPosition = modelViewMatrix * vec4( vPos, 1.0 );
gl_PointSize = 4.;
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
uniform sampler2D texture;
void main()
gl_FragColor = vec4( color, 1.0 );
// 顶点颜色应用上2D纹理
gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord );
}
</script>
Супер простое резюме вышеописанного процесса:
(1) Загружаются две геометрии, и они становятся кэшированными геометриями (более эффективно)
(2) Создайте систему частиц с моделью с большим количеством точек и сохраните позиции вершин другой модели вposition2
Атрибуты
(3) js только поддерживается и изменяетсяval
Значение передается вершинному шейдеру, и точка затенения вычисляется в соответствии со значением val, так что координаты вершин находятся вposition(顶点多的模型的顶点位置)
а такжеposition2(顶点少的模型的顶点位置)
переход между.
Эффект следующий:
2. Преобразование размера частиц
В предыдущем коде он передавался в шейдерsize
атрибут, но не используется, теперь используйте js, чтобы изменить это значение и передать его шейдеру.
Измените значение размера по времени в функции рендеринга цикла:
update() {
TWEEN.update();
this.stats.update();
// 动态改变size大小
let time = Date.now() * 0.005;
if (this.particleSystem) {
let bufferObj = this.particleSystem.geometry;
// 粒子系统缓缓旋转
this.particleSystem.rotation.y = 0.01 * time;
let sizes = bufferObj.attributes.size.array;
let len = sizes.length;
for (let i = 0; i < len; i++) {
sizes[i] = 1.5 * (2.0 + Math.sin(0.02 * i + time));
}
// 需指定属性需要被更新
bufferObj.attributes.size.needsUpdate = true;
}
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(() => {
this.update()
});
}
существуетvertex shader
, просто поставь
gl_PointSize = 4.;
заменить
// size在之前已经用attribute声明过了
gl_PointSize = size;
Затем эффект преобразования частиц имеет небольшое мерцание (загрузка изображения в формате GIF сжата, поэтому кажется, что он воспроизводится быстро):
3. Эффект размытия частиц
Здесь, чтобы имитировать эффект, что чем дальше частицы, тем более размыты частицы.模型视图坐标
, то есть координаты частицы, видимые из камеры, а затем вычислить значение оси z частицы в соответствии с ее значением оси z.尺寸
а также透明度
,Покинуть视线越远
частицы так, чтобы尺寸越大
,в то же время透明度越小
.
<script type="x-shader/x-vertex" id="vertexshader">
attribute float size;
attribute vec3 position2;
uniform float val;
// 颜色透明度
varying float opacity;
void main() {
// 开始产生模糊的z轴分界
float border = -150.0;
// 最模糊的z轴分界
float min_border = -160.0;
// 最大透明度
float max_opacity = 1.0;
// 最小透明度
float min_opacity = 0.03;
// 模糊增加的粒子尺寸范围
float sizeAdd = 20.0;
vec3 vPos;
vPos.x = position.x * val + position2.x * (1.-val);
vPos.y = position.y* val + position2.y * (1.-val);
vPos.z = position.z* val + position2.z * (1.-val);
vec4 mvPosition = modelViewMatrix * vec4( vPos, 1.0 );
// z轴坐标越小越模糊,即越远越模糊
if(mvPosition.z > border){
opacity = max_opacity;
gl_PointSize = size;
}else if(mvPosition.z < min_border){
opacity = min_opacity;
gl_PointSize = size + sizeAdd;
}else{
// 模糊程度随距离远近线性增长
float percent = (border - mvPosition.z)/(border - min_border);
opacity = (1.0-percent) * (max_opacity - min_opacity) + min_opacity;
gl_PointSize = percent * (sizeAdd) + size;
}
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
uniform sampler2D texture;
varying float opacity;
void main() {
// 根据传递过来的透明度值设置颜色
gl_FragColor = vec4(color, opacity);
gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord );
}
</script>
Эффект после трансформации следующий:
Конечно, мы можем временно убрать анимацию трансформации модели и перетаскивать модель прямо мышкой, чтобы лучше наблюдать изменение частиц с расстоянием.4. Распределение цвета частиц
(Простите, что снова кинул код)
Сделал небольшое изменение здесь, добавивvarying vec3 vColor
,существуетvertex shader
рассчитывается на основе координаты вершины по оси Y в системе координат вида моделиvColor
значение и передаетсяfragment shader
, так что частицы образуют латерально распределенные цветные полосы.
<script type="x-shader/x-vertex" id="vertexshader">
attribute float size;
attribute vec3 position2;
uniform float val;
// 颜色透明度
varying float opacity;
// 传递给片元着色器的颜色值
varying vec3 vColor;
void main() {
// 开始产生模糊的z轴分界
float border = -150.0;
// 最模糊的z轴分界
float min_border = -160.0;
// 最大透明度
float max_opacity = 1.0;
// 最小透明度
float min_opacity = 0.03;
// 模糊增加的粒子尺寸范围
float sizeAdd = 20.0;
vec3 vPos;
vPos.x = position.x * val + position2.x * (1.-val);
vPos.y = position.y* val + position2.y * (1.-val);
vPos.z = position.z* val + position2.z * (1.-val);
vec4 mvPosition = modelViewMatrix * vec4( vPos, 1.0 );
// z轴坐标越小越模糊,即越远越模糊
if(mvPosition.z > border){
opacity = max_opacity;
gl_PointSize = size;
}else if(mvPosition.z < min_border){
opacity = min_opacity;
gl_PointSize = size + sizeAdd;
}else{
// 模糊程度随距离远近线性增长
float percent = (border - mvPosition.z)/(border - min_border);
opacity = (1.0-percent) * (max_opacity - min_opacity) + min_opacity;
gl_PointSize = percent * (sizeAdd) + size;
}
float positionY = vPos.y;
// 根据y轴坐标计算传递的顶点颜色值
vColor.x = abs(sin(positionY));
vColor.y = abs(cos(positionY));
vColor.z = abs(cos(positionY));
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
uniform sampler2D texture;
varying float opacity;
varying vec3 vColor;
void main() {
// 根据传递过来的颜色及透明度值计算最终颜色
gl_FragColor = vec4(vColor * color, opacity);
gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord );
}
</script>
5. Преобразование цвета частиц
Неизменяемый цвет слегка монотонен, а затем значение атрибута частицы при выполнении анимации изменяется, и новое значение цвета частицы генерируется случайным образом после каждого раунда анимации, а затем старый цвет переходит в новый цвет во время преобразование модели частиц.
let tween = new TWEEN.Tween(pos).to({
val: 0
}, 1500).easing(TWEEN.Easing.Quadratic.InOut).delay(2000).onUpdate(updateCallback).onComplete(completeCallBack.bind(pos, 'go'));
let tweenBack = new TWEEN.Tween(pos).to({
val: 1
}, 1500).easing(TWEEN.Easing.Quadratic.InOut).delay(2000).onUpdate(updateCallback).onComplete(completeCallBack.bind(pos, 'back'));
tween.chain(tweenBack);
tweenBack.chain(tween);
tween.start();
// 动画持续更新的回调函数
function updateCallback() {
particleSystem.material.uniforms.val.value = this.val;
// 颜色过渡
if (this.nextcolor) {
let val = (this.order === 'back' ? (1 - this.val) : this.val);
let uColor = particleSystem.material.uniforms.color.value;
uColor.r = this.color.r + (this.nextcolor.r - this.color.r) * val;
uColor.b = this.color.b + (this.nextcolor.b - this.color.b) * val;
uColor.g = this.color.g + (this.nextcolor.g - this.color.g) * val;
}
}
// 每轮动画完成时的回调函数
function completeCallBack(order) {
let uColor = particleSystem.material.uniforms.color.value;
// 保存动画顺序状态
this.order = order;
// 保存旧的粒子颜色
this.color = {
r: uColor.r,
b: uColor.b,
g: uColor.g
}
// 随机生成将要变换后的粒子颜色
this.nextcolor = {
r: Math.random(),
b: Math.random(),
g: Math.random()
}
}
this.scene.add(particleSystem);
this.particleSystem = particleSystem;
Пока что мы реализовали эффекты частиц, которые появились в начале статьи, шаг за шагом!
(не могу не поставить картинку еще раз)
Конечно, математика лучше, в сочетании с некоторыми волшебными нелинейными функциями, вы можете воспроизводить более великолепные эффекты~
Это почти закончено сейчас
Я не ожидал, что эта статья будет неостановимой...(´-ι_-`)
Спасибо, что смотрели, как я терпеливо наблюдаю за тем, как я так много болтаю~
Хотя это только верхушка айсберга в виртуальном трехмерном мире веб-страниц, они могут быть полезны заинтересованным новичкам, и цель этой статьи достигнута; я должен признать, что я только новичок, который прочитал больше информацию от себя.Если в статье есть какие-либо недоразумения, я надеюсь, что проходящий Бог напомнит вам, увидев это с первого взгляда, заранее спасибо~
Адрес кода: (скопируйте его и запустите на сервере)
Звезда терпеливо ждет, чтобы ее ткнули, вы видите, она выглядит так: ★
Предварительный просмотр портала онлайн
--------- Разделительная линия благодарностей ---------
Эта статья основана на некоторых фрагментах кода и изображений из следующих двух статей, чтобы отдать дань уважения предшественникам:
Эффекты частиц three.js (на основе процессора и графического процессора соответственно)
Рендеринг мультфильмов (Часть 1)
--------- Разделительная линия объявления ---------
Прошлые абзацы:
Путь к пробуждению консоли, как насчет печати анимации?
узел гусеничный фонд, самостоятельный и самостоятельный, чтобы понять?