передняя часть Seewo ENOW
Официальный сайт компании:CVTE (Гуанчжоу CVTE)
Команда: enow team центра программных платформ Seewo для будущего образования в рамках CVTE
Автор этой статьи:
Введение
Зачем писать эту серию?Существует бесконечное количество вводных руководств по 3D-рендерингу на рынке, некоторые из которых посвящены принципам математических расчетов, некоторые ориентированы на введение конвейерного процесса рендеринга, а некоторые рассказывают вам, как использовать API и как быстро начать.
В этой серии статей мы надеемся рассмотреть создание механизма 3D-рендеринга с нуля и шаг за шагом построить необходимые элементы и процессы для 3D-рендеринга, чтобы читатели могли «узнать причину и причину» в этой стремительной среде. прикосновение прохлады, я надеюсь, что каждый может что-то получить, добро пожаловать на обмен и обсуждение.
В этой серии статей демонстрация кода используется как ключ к глубокому пониманию элементов и связей 3D-рендеринга из процесса построения этой демонстрации.Он имеет следующие характеристики:
1. Не используйте технологию webgl для завершения 3D-рендеринга. Спецификация webgl помогает нам инкапсулировать многие низкоуровневые реализации, поэтому она также скрывает некоторые важные детали. Автор надеется, что технология webgl предоставляет только интерфейс для подключения к вычислениям на GPU, чтобы мы можем использовать мощность графического процессора для повышения эффективности вычислений, конечно, то же самое можно сделать без использования графического процессора и только с использованием процессора, но это менее эффективно, но этого достаточно для учебных целей, но это сделает нас более понятными, поэтому мы будет использовать чистый код JS для выполнения всех расчетов и рендеринга и, наконец, реализовать «движок 3D-рендеринга»;
2. Мы используем процесс построения демо, чтобы понять 3D-рендеринг, поэтому в этом процессе мы разделим его на несколько небольших этапов, каждый этап определяется поэтапной целью, а иногда для достижения цели используется более простой метод.Сексуальные цели становятся более сложным и может отодвигать частичные реализации для удовлетворения более сложных потребностей;
3. Поскольку в качестве подсказки и основной части используется Demo, все коды доступны в этом репозитории (GitHub.com/S Несколько Эли Ю/…), я надеюсь, что каждый сможет скачать и запустить его или даже собрать его с нуля, я верю, что вы можете что-то получить!
Затем мы сначала покажем нашу серию конечных целей: мы реализуем механизм 3D-рендеринга на основе API 2D-рендеринга.Он может анализировать и отображать широко используемые форматы данных 3D-моделей на рынке с функциями рендеринга границ/рендеринга фрагментов/текстурирования. /световые тени и т.д. Пожалуйста, смотрите следующие визуализации:
Эффект первый:
Эффект второй:
визуализатор каркаса
маленькая цель
Конечно, если вы ожидаете, что статья позволит читателям создать такой движок рендеринга с нуля, то вы можете помыться и спать.
Поэтому нам нужно разложить эту конечную цель, и сначала поставить маленькую цель: каркасный рендерер.Конкретно, если на вход модель куба, то такой эффект рендеринга можно вывести.(Едва видно, что это куб)
Введение демо-склада
Вы можете идти по стопам автора шаг за шагом, чтобы достичь этих небольших целей самостоятельно, или вы можете получить общее представление на лету. Если вы хотите потратить время на совместное обучение, вы можете открыть этот репозиторий:GitHub.com/S Несколько Эли Ю/…
В корневом пути есть index.html, и когда вы его откроете, вы увидите страницу каталога, на которой собраны все эксперименты.Поскольку были проведены только первые три эксперимента, интерфейс, который вы видите, показан на рисунке ниже.
Структура каталогов хранилища выглядит следующим образом: index.html — это страница каталога, common — общедоступная функция, 0/1.1/1.2 — небольшая папка для экспериментов. классов, просто откройте index.html для предварительного просмотра и отладки демо.
начать с линии
Как и в случае с так называемым «два в одной жизни, два в трех, три во всем», рендеринг графики всегда начинается с рисования линии (код в этом разделе может относиться к demo0).
const main = () => {
const c = document.querySelector('#canvas')
const canvas = Canvas.new(c)
canvas.drawline(Vector.new(50, 50), Vector.new(100, 100), Color.green())
}
Холст класс
Мы собираемся инкапсулировать класс Canvas, который отвечает за работу с DOM реального холста для рисования Прежде всего, его самая основная способность — рисовать линии.
Здесь мы используем API canvas.context для рисования сегментов линий. API холста очень прост и удобен в использовании, поэтому я не буду вдаваться в подробности. Читатели, которые не знакомы с ним, могут обратиться к нему здесь:
developer.Mozilla.org/this-cn/docs/…
class Canvas extends GObject {
constructor(canvas) {
super(canvas)
this.canvas = canvas
this.ctx = canvas.getContext('2d')
}
drawline(v1, v2, color) {
const ctx = this.ctx
ctx.beginPath()
// 设置颜色
ctx.strokeStyle = color.toRgba()
// 移动起点
ctx.moveTo(v1.x, v1.y)
// 连接到终点
ctx.lineTo(v2.x, v2.y)
ctx.closePath()
// 绘制path的stroke(边框)
ctx.stroke()
}
}
Векторный класс
Поскольку мы введем матричные операции позже, здесь сначала вводится понятие вектора.Вектор — это направленный отрезок линии, который может быть представлен x, y и z в трех измерениях.
Эта статья больше связана с рендерингом, и подробности некоторых вычислений могут быть не слишком расширены, поэтому конкретную реализацию некоторых методов я скрыл (но их можно найти в репозитории кода), а как реализовать имена методов, если вы умны.
Например, вычитание/модуль/нормализация/скалярное произведение/перекрестное произведение и т. д., здесь их можно игнорировать.
class Vector extends GObject {
// 表示三维点的类
constructor(x, y, z) {
super()
this.x = x
this.y = y
this.z = z
}
}
Цветовой класс
Класс Color еще проще, мы используем rgba для описания цвета и инкапсулируем методы преобразования разных форматов, а также общие цвета.
class Color extends GObject {
// 表示颜色的类
constructor(r, g, b, a) {
super()
this.r = r
this.g = g
this.b = b
this.a = a
}
toRgba() {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`
}
// 常见的几个颜色
static black() {
}
static green() {
}
static blue() {
}
}
demo0
С помощью приведенных выше классов вы можете нарисовать линию и получить следующую картинку, которая кажется немного... простой Не волнуйтесь, она скоро взлетит.
const main = () => {
const c = document.querySelector('#canvas')
const canvas = Canvas.new(c)
canvas.drawline(Vector.new(50, 50), Vector.new(100, 100), Color.green())
}
Нарисуйте сетку, вид спереди
Определение сетки
Что такое сетка С точки зрения изображения упомянутую выше модель пистолета можно разделить на две части: черная линия рамки и зеленый элемент поверхности.
Черная линия рамки — это тот меш, о котором мы говорим, и каждый из этих мешей — не прямоугольник, а треугольник.
Зачем использовать треугольник?Потому что три точки треугольника должны быть расположены на одной плоскости, поэтому это наименьшая единица, используемая для описания плоскости.С другой стороны, четырехугольник не имеет места, потому что три точки определяют грань , поэтому четвертая точка может быть падением. Она находится вне этой поверхности, поэтому, если она описана четырехугольником, она может быть одной или двумя плоскостями, поэтому это неудобно.
Класс сетки
С этой целью мы вводим класс Mesh для его описания, который в основном состоит из двух внутренних переменных, вершин и индексов (код в этом разделе может относиться к demo1.1).
Далее мы используем куб для иллюстрации, вершины содержат все вершины геометрии (8 вершин, соответственно от v0 до v7), используя правую систему координат, позиционные отношения вершин следующие.
Индексы — это вложенный массив, и каждый элемент описывает треугольную грань, поэтому он содержит соответственно три числа, что означает, что три вершины этой треугольной грани являются первыми точками в массиве вершин (индекс точки).
Этот метод описания не создан автором, автор наблюдал за подавляющим большинством форматов данных 3D-моделей на рынке (будет раздел, посвященный анализу форматов данных 3D-моделей, где мы его расширим и пропустим). здесь), в основном все, чтобы использовать точку + индекс для описания поверхности.
Итак, согласно данным индексов, v0 v1 v2 образуют первую грань, v1 v3 v2 образуют вторую грань, ... и так далее.
class Mesh extends GObject {
// 表示三维物体的类
constructor() {
super()
this.position = Vector.new(0, 0, 0)
this.rotation = Vector.new(0, 0, 0)
this.scale = Vector.new(1, 1, 1)
this.vertices = null
this.indices = null
}
// 返回一个正方体
//
// v0----- v1
// /| /|
// v4------v5|
// | | | |
// | |v2---|-|v3
// |/ |/
// v6------v7
//
// Coordinates
// 右手坐标系
// Y
// |
// |
// O ------ X
// /
// Z
//
static cube() {
// 8 points
let points = [
-1, 1, -1, // 0
1, 1, -1, // 1
-1, -1, -1, // 2
1, -1, -1, // 3
-1, 1, 1, // 4
1, 1, 1, // 5
-1, -1, 1, // 6
1, -1, 1, // 7
]
let vertices = []
for (let i = 0; i < points.length; i += 3) {
let v = Vector.new(points[i], points[i + 1], points[i + 2])
let c = Color.randomColor()
vertices.push(Vertex.new(v, c))
}
// 12 triangles * 3 vertices each = 36 vertex indices
let indices = [
// 12
[0, 1, 2],
[1, 3, 2],
[1, 7, 3],
[1, 5, 7],
[5, 6, 7],
[5, 4, 6],
[4, 0, 6],
[0, 2, 6],
[0, 4, 5],
[5, 1, 0],
[2, 3, 7],
[2, 7, 6],
]
let m = this.new()
m.vertices = vertices
m.indices = indices
return m
}
}
Определение сетки здесь очень важно, и последующее содержание будет основываться на этом определении, поэтому обязательно уделите некоторое время его пониманию.
Здесь внимательные ученики обнаружат, что вершины включают не только вектор, но и цвет, цвет здесь не будет использоваться для рисования каркасов, а будет использоваться при рисовании фрагментов позже.
class Vertex extends GObject {
// 表示顶点的类, 包含 Vector 和 Color
// 表示了一个坐标和一个颜色
constructor(position, color) {
super()
this.position = position
this.color = color
}
}
метод DrawMesh
Вышеизложенное — это только базовые знания о сетке.В этой небольшой демонстрации мы ожидаем, что в экземпляре Canvas есть метод drawMesh, который может рисовать все виды различных сеток.Поэтому самое главное, давайте взглянем на конкретная реализация drawMesh.
canvas.drawMesh(Mesh.cube())
class Canvas extends GObject {
constructor(canvas) {
}
drawline(v1, v2, color) {
}
drawMesh(mesh) {
const { indices, vertices } = mesh
// 偏移的魔数
const position = Vector.new(150, 100, 0)
const color = Color.blue()
indices.forEach(ind => {
const [v1, v2, v3] = ind.map(i => {
// 强行缩放的魔数
return vertices[i].position.multi_num(30).add(position)
})
this.drawline(v1, v2, color)
this.drawline(v2, v3, color)
this.drawline(v1, v3, color)
})
}
}
demo1.1
В этой демонстрации мы рисуем «вид спереди» геометрии, поэтому мы напрямую игнорируем значение z каждой вершины и рисуем ее прямо на плоскости холста, эффект такой же, как если бы вы наступили на выброшенную коробку Tetra Pak, прямо «раздавленную». .При этом для удобства наблюдения мы принудительно добавили магические числа смещения и увеличения.Пока что получается такой рендеринг.
(⊙o⊙)…Ну, кажется, он немного ест корень лотоса, не то, что я ожидал.Почему?После наблюдения самое главное, что вид спереди не имеет чувства перспективы, спереди и сзади полностью сложены вместе, а сбоку вообще не видно. ...
Тогда давайте добавим эффект перспективы!!!
Нарисуйте повернутую перспективу
шаги декомпозиции demo1.2
Хотя в демо выше можно увидеть вид куба спереди, но на самом деле в нем есть два магических числа: увеличение в 30 раз, смещение (150, 100).Давайте сначала удалим эти два уродливых магических числа и посмотрим, что получится, как.
В этом разделе используется много матричных методов.Легко понять, что матрицы используются для преобразования (включая вращение/проекцию/смещение/равномерную деформацию), и множественные матричные преобразования могут быть получены путем умножения матриц для получения новой матрицы. Множественные преобразования, эта вторая функция делает матрицы очень удобными в использовании, она может «упаковывать» сложные преобразования вместе.
Здесь будет много места, чтобы расширить матричные операции, автор не будет слишком расширять это сначала, и, если необходимо, я добавлю больше деталей позже.
Здесь мы можем открыть демонстрацию demo1.2, которая является очень важной демонстрацией, поскольку работа с матрицей относительно абстрактна, поэтому эта демонстрация использует шаг декомпозиции для комментирования части кода (строки с 24 по 27 блока кода ниже), а затем мы медленно развяжем его, чтобы увидеть эффект от каждого шага.
class Canvas extends GObject {
constructor(canvas) {
//...
}
drawMesh(mesh, cameraIndex) {
const { indices, vertices } = mesh
const { w, h } = this
let { position, target, up } = Camera.new(cameraIndex || 0)
const rotation = Matrix.rotation(mesh.rotation)
const translation = Matrix.translation(mesh.position)
const view = Matrix.lookAtLH(position, target, up)
const view2 = Matrix.lookAtLH(position, target, Vector.new(1, 1, 0))
const projection = Matrix.perspectiveFovLH(8, w / h, 0.1, 1)
const world = rotation.multiply(translation)
const worldAndView = world.multiply(view)
const worldAndView2 = world.multiply(view2)
const transform = worldAndView.multiply(projection)
let t = undefined
let needProject = false
// needProject = true
// t = world
// t = worldAndView2
// t = transform
console.log('transform', t)
const color = Color.blue()
indices.forEach(ind => {
const [v1, v2, v3] = ind.map(i => {
if (needProject) {
return this.project(vertices[i], t).position
} else {
return vertices[i].position
}
})
this.drawline(v1, v2, color)
this.drawline(v2, v3, color)
this.drawline(v1, v3, color)
})
}
}
Теперь запустите demo1.2, и вы должны увидеть такой эффект:
Красная стрелка указывает на сетку, которую мы нарисовали. Почему она такая маленькая? Потому что положения вершин куба все от -1 до 1, а длина стороны куба равна 2. Размер холста300*200, поэтому куб, естественно, маленький и невидимый.
адаптивный размер
Чтобы это исправить, распаковываем строку комментариев:needProject = true
class Canvas extends GObject {
constructor(canvas) {
//...
}
project(coordVertex, transformMatrix = Matrix.identity()) {
}
drawMesh(mesh, cameraIndex) {
// 这里省略....
let t = undefined
let needProject = false
needProject = true
// t = world
// t = worldAndView2
// t = transform
// 这里也省略....
}
}
Таким образом, положение вершин будет скорректировано перед визуализацией подкаркаса.Конкретный метод проекта заключается в следующем.Как видно из кода, исходный диапазон ширины и высоты будет изменен с -1 на От 1 до 0 до w и от 0 до h. Таким образом, как только геометрия заполнит весь вид!
class Canvas extends GObject {
project(coordVertex, transformMatrix = Matrix.identity()) {
const { w, h } = this
const [w2, h2] = [w / 2, h / 2]
const point = transformMatrix.transform(coordVertex.position)
const x = point.x * w2 + w2
const y = - point.y * h2 + h2
const z = point.z
const v = Vector.new(x, y, z)
return Vertex.new(v, this.color)
}
drawMesh(mesh, cameraIndex) {
// 省略
let t = undefined
let needProject = false
needProject = true
// t = world
// t = worldAndView2
// t = transform
indices.forEach(ind => {
const [v1, v2, v3] = ind.map(i => {
if (needProject) {
return this.project(vertices[i], t).position
} else {
return vertices[i].position
}
})
// 省略
})
}
}
Таким образом, вы можете получить следующие рендеры
Вращение каркаса
Далее заставим эту картинку двигаться, и регулярно меняем атрибут вращения меша.Конечно, это все написано в demo1.2!
const main = () => {
const c = document.querySelector('#canvas')
const canvas = Canvas.new(c)
console.log(canvas)
canvas.drawline(Vector.new(50, 50), Vector.new(100, 100), Color.green())
const mesh = Mesh.cube()
canvas.drawMesh(mesh)
setInterval(() => {
canvas.clear()
mesh.rotate(new Vector(0.05, 0.1, 0))
console.log('r', mesh.rotation)
canvas.drawMesh(mesh, 3)
}, 200)
}
Затем мы распаковываем строку комментариев кода:t = world
class Canvas extends GObject {
// 省略
drawMesh(mesh, cameraIndex) {
// 省略
let t = undefined
let needProject = false
needProject = true
t = world
// t = worldAndView2
// t = transform
// 省略
}
}
Таким образом, вы можете увидеть эффект вращения каркаса.
Ну, это все еще немного некрасиво. Наверное, потому что:
- Нет эффекта перспективы
- Камера слишком близко, она полностью противоречит геометрии
В таком случае давайте начнем заботиться об объективе!
Класс камеры
Если это вид спереди, почти нет необходимости иметь понятие «линза», потому что положение и направление линзы определяются, расстояние также становится неактуальным.
Но это сильно отличается от известного трехмерного мира, наблюдаемого нашими человеческими глазами, поэтому нам нужно ввести класс камеры для его описания, чтобы можно было получить вид в перспективе.
Класс Camera включает три переменные, положение цели вверху и тип Vector.
class Camera extends GObject {
constructor(index) {
super()
this.init(index)
}
init(index) {
const dict = {
0: () => {
this.position = Vector.new(0, 0, -10)
this.target = Vector.new(0, 0, 0)
this.up = Vector.new(0, 1, 0)
},
// 忽略
}
dict[index || 0]()
}
}
Эти три переменные: позиция относится к положению камеры, цель относится к цели, на которую смотрит камера, связь между позицией и целью представляет собой направление оси Z самой камеры, а вверх относится к восходящему направлению камеры. камера.
Сколько степеней свободы здесь описывает камера?
Я посчитал, должно быть 6.
Положение камеры (начало системы отсчета камеры) определяется тремя степенями свободы.
Направление декартовой системы координат камеры определяется тремя степенями свободы.Просто понятно, что когда положение камеры фиксировано, камера может качаться влево и вправо, вверх и вниз, поэтому цель может определить эти два градуса После того, как цель зафиксирована, камера также может вращаться вокруг своей собственной оси Z, переменная up ограничивает эту степень свободы, поэтому камера определяется однозначно.
Видно, что 9 значений используются для определения 6 степеней свободы, так что это не самое лаконичное описание, и 9 значений не полностью независимы (ортогональны), а из-за самой семантики и легкой отладки , мы продолжаем использовать это описание.
Это немного абстрактно, поэтому автор разместил еще несколько ссылок на описания этих параметров для вашего ознакомления.Если вы все еще не понимаете, пожалуйста, свяжитесь со мной и помогите мне улучшить вместе.
Применить матрицу объектива
Затем мы раскомментируем еще одну строку кода: t = worldAndView2
class Canvas extends GObject {
// 省略
drawMesh(mesh, cameraIndex) {
// 省略
const view2 = Matrix.lookAtLH(position, target, Vector.new(1, 1, 0))
const worldAndView2 = world.multiply(view2)
// 省略
let t = undefined
let needProject = false
needProject = true
t = world
t = worldAndView2
// t = transform
// 省略
}
}
В это время вы можете получить каркас, который вращается под этим углом.Это эквивалентно снимку, сделанному объективом "наклонив голову", что является эффектом параметра вверх.Вы можете настроить три камеры обзора2 больше здесь , параметры, чтобы испытать роль положения и цели.
перспектива
Если вы действительно отрегулируете положение и цель, вы обнаружите странные вещи, независимо от того, близкое это положение или далекое, изображение не будет увеличиваться и уменьшаться, а всегда будет поддерживать текущий размер.
Это связано с концепцией визуального пространства, мы сейчас рисуем «вид спереди», визуальное пространство вида спереди представляет собой прямоугольный параллелепипед, поэтому объекты одинакового размера вблизи и вдали будут полностью перекрываться, нет «ближнего». большое и далеко маленькое». Кажется, что пространство «сжато» вместе. Зрительное пространство нормального человеческого глаза представляет собой не прямоугольный параллелепипед, а «конус», как показано на следующем рисунке.
Чтобы выполнить это требование, мы раскомментируем строку кода:t = transform
class Canvas extends GObject {
// 省略
drawMesh(mesh, cameraIndex) {
// 省略
const view2 = Matrix.lookAtLH(position, target, Vector.new(1, 1, 0))
const worldAndView2 = world.multiply(view2)
// 省略
let t = undefined
let needProject = false
needProject = true
t = world
t = worldAndView2
t = transform
// 省略
}
}
Вы можете получить следующие эффекты:
Вкратце, есть несколько шагов для расчета:
- Item Self Matrix (обработка собственного смещения и поворота элемента)
- Матрица камеры (используется для преобразования мировой системы координат в систему координат камеры)
- Матрица настройки перспективы (отрегулируйте соотношение ближнего, большого, дальнего, малого / ширины и высоты, чтобы изображение было близко к человеческому зрению)
- Нормализация холста (пусть изображение заполняет холст холста).
Путем преобразования вышеперечисленных шагов исходное описание сетки, основанное на собственной системе отсчета объекта, может быть преобразовано в описание, основанное на системе отсчета перспективы камеры, то есть сетка, нарисованная после преобразования, является тем, что видит человек. глаз видит положение и направление камеры.
Если вы будете следовать нашему руководству шаг за шагом, чтобы разгадать комментарии demo1.2, вы можете получить каркас вращающегося куба, то есть эффект прямого запуска demo1.3.
резюме
В этой статье мы пишем каждую строку кода с нуля до тех пор, пока не будет отрисован перспективный вид повернутой геометрии.
В этом процессе мы столкнулись с API рисования линий 2D-контекста холста и, основываясь на этом, инкапсулировали самый важный метод класса Canvas, drawMesh. Сначала мы реализовали простую версию (вид спереди) для рендеринга сетки, в порядок описания Mesh (геометрии) Мы ввели такие классы, как Vector/Color/Vertice/Mesh.
Позже, чтобы нарисовать вид в перспективе, мы обновили метод drawMesh, ввели класс Camera и множество матричных операций, и, наконец, мы получили перспективный вид повернутой геометрии! ! !
Полезна ли эта статья для вас?Знаете ли вы ее давно, или вы можете видеть сквозь нее с первого взгляда, находитесь ли вы в тумане или на лошади, вы можете обсудить и обменяться со мной. .
В следующем выпуске мы в основном поговорим о рендеринге фрагментов, и разберемся с интерполяцией цвета и положения, а также обработкой отношения окклюзии, а также добавим соответствующие детали взаимодействия и комментарии к этому уроку.Надеюсь, что нет , в следующий раз.