[JavaScript, WebGL] Знакомство фронтендера с WebGL: рефакторинг, анимация (часть 4)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Это история в несколько частей:
- Знакомство фронтендера с WebGL: почему WebGL? (часть 1)
- Знакомство фронтендера с WebGL: первые наброски (часть 2)
- Знакомство фронтендера с WebGL: четкие линии (часть 3)
- Знакомство фронтендера с WebGL: рефакторинг, анимация (часть 4)
Данную статью пишу спустя полтора года, поэтому налет свежести потерян.Эта статья уже больше про код, меньше про страдания. Здесь по сути я выложу итоговый результат. Вот это красивое яблоко:Извините, данный ресурс не поддреживается. :( Соберем весь опытТак, настало время рефачить и добавить анимации с интерактивом. По задумке дизайнера, интерактива особо не было, просто реакция на мышку.За 3 статьи у меня появилось много полезных инструментов которые нужно объединить в какой-то более или менее удобный интерфейс. Что у меня есть:
- Фрагментный шейдер который умеет правильно зарисовывать грани.
- Парсер .obj который поможет правильно разобрать модельку на вершины, uv и индексы.
- Набор хелперов от WebGL Fundamentals.
- animejs для анимации
- gl-matrix для рассчетов матрицы
- Сама моделька
На самом деле когда я уже закончил с правильным рендером модельки, я знал как буду рефачить код. В голове картина уже была.
Во-первых, это должно быть обязательно на классах! Почему на классах? Синтаксис красивый, а еще пригодится много контекста.
Во-вторых, я буду использовать для анимации animejs, легкая библиотека для анимации с удобным апи (для меня).
В-третьих, я буду парсить и кешировать модель загруженную модель в indexedDb (мне тогда казалось это крутой идеей и оптимизацией, а так же я просто хотел воспользоваться этим апи).Написанной мной шейдер был универсальным, ему было все равно какую модель ему загрузить, главное чтоб faces (индексы) состояли из квадратов, потому что если будут из каких-нить треугольников или пятиугольников, то весь мой парсер и шейдер полетит к чертям.По песочнице можно было понять, что вопроса как именно смещать фигуру для анимации у меня не было. Я просто высчитываю матрицу и загружаю ее в вершинный шейдер, а он уже сам все там рассчитывает и высчитывает точки.Как вообще выглядит анимация в webgl?
- Делаешь расчет переменных.
- Загружаешь их в шейдер.
- Вызываешь drawArray, чтоб карта нарисовала на основе переменных новую картинку.
- Повторять 1 пункт до тех пор, пока нужна анимация.
Вот так вот, если в css нам достаточно поменять значение, то в webgl нам надо поменять значение, а потом запустить рендер.Так же была еще особенность. Мне нужно было анимировать разные части модельки по-разному.Как я модели делил на части.Дизайнер нарисовал более сложные фигуры чем просто яблоко и он хотел, чтоб все каждая часть фигуры вертелась в нужную сторону. А для этого нужно было делить модели на части, то есть разделить огромный массив вершин по частям, на каждую часть применять свою матрицу и рисовать ее. В webgl по умолчанию полотно не стирается и если вы вызываете drawArray, то он просто нарисует пикселями поверх того, что уже было нарисовано. Благодаря этому, можно загружать часть модельки, что-то с ней делать, рисовать, а потом другую часть и так по кругу.А как поделить модель на несколько деталей?В 3д редакторах наборы треугольников можно собирать в группы, а при экспорте эти группы выделены с помощью символа o (object) в .obj, таким образом я мог распарсить модельку на несколько отдельных моделек.Я выделил для себя 3 сущности:
- Матрицы
- Модели (Mesh)
- Сам рендер
Начнем с простого, класса матрицы, я просто хотел какие-то красивые методы со знакомыми названиями, внутри которых будет магия матриц. Почему вообще матрицы высчитываем на стороне js, а не webgl? Потому, что матрицы нужно динамически подключат и отключать, а в статичных условиях шейдеров этого не сделать. Так как это математика, я посчитал, что должен кешировать результат вычислений. А потом с помощью метода получать результат работы.
import { glMatrix, mat4, vec3 } from "gl-matrix"; // либа которая поможет складывать
export class Matrix {
// я решил добавить коллбек который будет вызываться каждый раз когда какой-то метод в классе вызывали.
constructor(onUpdate = () => {}) {}
// объект с матрицами, по сути кеш и поможет в правильном порядке собирать матрицы, а это очень важно!
#matrices = {};
// настройка камеры, до сих пор не разобрался как это работает, но позволяет разместить фигуру на нужной дистанции.
setOrtho(left, right, bottom, top, near, far) {}
// значение по умолчанию
#scale = 1;
// сеттер для управление scale
setScale(ratio) {}
// решил сделать геттор для получение актуального значения,
// чтоб в будущем использовать для всяких рассчетов с анимацией.
getScale() {}
#translate = [0, 0, 0];
/**
* x, y, z смещение
* @param {[number, number, number]}params
*/
setTranslate(params) {}
getTranslate() {}
// ну и rotate, не объединял в один метод, потому, собирался анимировать каждое это значение отдельно.
// если мне важно было знать для реализации translate и scale, то на rotate было все равно, поэтому геттеров не делал. Зачем?
setRotateX(deg) {}
setRotateY(deg) {}
setRotateZ(deg) {}
// перемножаем все матрицы и получаем итоговую которую нужно прокинуть в шейдер
getCurrent() {}
}
Я решил не погружаться в реализацию методов, потому, что в конце выложу просто песочницу со всем кодом.Дальше у нас класс для моделей. Я назвал его Mesh, потому что увидел такое название в Pixi.js. Mesh значит сетка, сетка треугольников. Поняли тему? Треугольники вершина всего!Класс получает распарсенные вершины и uv, инициализирует в себе матрицу, а так же загружает в gl буферы данных, чтоб потом можно было легко меняться между ними. Буфер это место, куда можно загрузить данные, а потом прокидывать этот буфер в атрибуты. Буферов можно инициализировать несколько и они все будут храниться в памяти, тратить на переинициализацию времени потом не нужно будет.
import { Matrix } from "./Matrix";
export class Mesh {
/**
*
* @param {Float32Array} positions
* @param {Float32Array} uv
*/
// получаем вершины и текстурные координаты
constructor({ positions, uv }) {
this.positions = positions;
this.uv = uv;
// указываем drawArrays сколько у нас треугольников, так как для треугольника всегда нужно 3 точки, то можем смело делить массив с точками на 3 и получим итоговое значение треугольников
this.count = this.positions.length / 3;
// личная матрица для модельки
this.matrix = new Matrix();
}
// ссылочки на буфера
positionsBuffer;
uvBuffer;
/**
* @param {WebGLRenderingContext} gl
*/
// придумал для себя такой способ как прокидывать контекст для модельки, мне хотелось чтоб нужные классы моделей были уже созданы, их нужно только прокинуть в класс рендера.
attachRender(gl) {
this.gl = gl;
}
/**
* Создаем буфера, загружаем в них данные, храним эти буферы потом в классе, чтоб легко достать
*/
initializeBuffers() {}
}
В общем больше ничего и не нужно. Получи вершины, загрузи в буфера и дай ссылки на них.ModelRenderВся основная нагрузка лежит на классе рендера. Он будет инициализировать в выданный ему canvas контекст webgl, переменные, атрибуты для шейдеров. Включать всякие экстеншены, настройки, хранить в себе модели которые ему нужно в данный момент отрендерить, настройки цвета, толщины линии моделки, следить за ресайзом экрана, настраивать глобальную камеру и в конце вызывать самый важный метод drawArrays! Благодаря этому волшебному методу, карта и начинает рисовать изображение на холсте.
// вершинный шейдер
precision mediump float;
uniform mat4 uMeshMatrix; // смещение модели
uniform mat4 uCameraMatrix; // мировая камера
attribute vec4 aPosition; // вершины
attribute vec2 aTextureCoords; // текстурные координаты
varying vec2 vTextureCoords; // интерполированные переменные,
// на каждый пиксель между вершинами передаются интерполированные значения текстурных координат в фрагментный шейдер
void main(){
gl_Position = uCameraMatrix * uMeshMatrix * aPosition;
vTextureCoords = aTextureCoords;
}
// фрагментный шейдер
#extension GL_OES_standard_derivatives : enable // включаем штуку которая делает наши линии красивыми
precision mediump float; // выставляем как нужно округлять float значения
uniform vec3 uLineColor; // цвет линии
uniform vec3 uBgColor; // цвет фона
uniform float uLineWidth; // ширина линии
varying vec2 vTextureCoords; // текстурные координаты которые нужны чтоб высчитать грани
// моя супер логика для расчета границ
// я не могу вспомнить как я это считал, кажется полтора года назад я был умней.
float border(vec2 uv, float uLineWidth, vec2 gap) {
// переменная gap нужна была, чтоб сделать линии более плавными, она рассчитывается динамически на основе текстурных координат благодаря директиве
// smoothstep получал точка A и точку B, а потом получал какое-то значение и на основе этого значения возвращал плавное соотношение между А И Б.
vec2 xy0 = smoothstep(vec2(uLineWidth) - gap, vec2(uLineWidth) + gap, uv);
vec2 xy1 = smoothstep(vec2(1. - uLineWidth) - gap, vec2(1. - uLineWidth) + gap, uv);
vec2 xy = xy0 - xy1;
return clamp(xy.x * xy.y, 0., 1.);
}
void main() {
vec2 uv = vTextureCoords;
vec2 fw = vec2(uLineWidth + 0.05);
#ifdef GL_OES_standard_derivatives
// прогрессивное улучшение, на случай если директива не работает
fw = fwidth(uv);
#endif
// получаем коэффициент который потом используем чтоб красить в нужным цветом.
float br = border(vTextureCoords, uLineWidth, fw);
// mix смешивает цвета, если значение 1 вернет полный цвет uLineColor, если 0, то вернет uBgColor.
// А если значение где-то посередине, то вернет какой-то общий цвет между ними двумя.
// ВИВА МАТЕМАТИКА
gl_FragColor = vec4(mix(uLineColor, uBgColor, br), 1.);
}
import { vertex, fragment } from "./shaders"; // никакой магии, там просто шейдеры в строках
import { Mesh } from "./Mesh";
import { Matrix } from "./Matrix";
import {
createProgramFromTexts,
resizeCanvasToDisplaySize,
saveRectAspectRatio,
} from "./helpers";
export class ModelRender {
// я предпочел чтоб класс сам создавал себе канвас, ему нужно просто указать куда его вставлять
canvas = document.createElement("canvas");
// получаем контекст, тут еще полифил для ie11
#gl = this.canvas.getContext("webgl");
// Получаем экстеншен, чтоб дальше его включать.
// Тут возвращается номер, который потом используется чтоб понять какой экстеншен нужно включить.
#derivatives = this.#gl.getExtension("OES_standard_derivatives");
// Создаем программу из любимых шейдеров
#program = createProgramFromTexts(this.#gl, vertex, fragment);
// удобные мапы для работы со ссылками на атрибуты
#attrs = {
position: this.#gl.getAttribLocation(this.#program, "aPosition"),
textureCoords: this.#gl.getAttribLocation(this.#program, "aTextureCoords"),
};
// мапа для работы ссылками на переменные
#uniforms = {
meshMatrixLocation: this.#gl.getUniformLocation(
this.#program,
"uMeshMatrix"
),
...
};
constructor() {
const gl = this.#gl;
// говорим карте проверять уровень треугольников, чтоб рисовалось только то что на переднем фоне
gl.enable(gl.DEPTH_TEST);
// загружаем программу в карту
gl.useProgram(this.#program);
// включаем атрибуты, ВСЕ НУЖНО ВКЛЮЧАТЬ
gl.enableVertexAttribArray(this.#attrs.position);
gl.enableVertexAttribArray(this.#attrs.textureCoords);
}
meshesByType = {};
models = {};
// я загружал сразу все модели в класс, а потом по ключу инициализировал нужную
#initializeModel = (type) => {};
// `div` элемент в который вставят `canvas`
/** @type {HTMLElement} */
holder;
#modelName;
// мне показалось прикольным переключаться между модельками с помощью сеттера
set modelName(type) {}
/**
* Сделал геттер, который возвращал текущую модель. Нужно, чтоб была возможность анимировать модельку. Менять ей состояние матрицы.
* @returns {Object<string, Mesh>}
*/
get currentModel() {
return this.meshesByType[this.#modelName]
}
// объект из которого читались цвет фона и линии, а так же толщина
// я заранее знал на какой странице какие цвета должны быть, поэтому не выносил этот объект в Mesh.
meshParams = {
bgColor: 0,
lineColor: 0,
lineWidth: 0.01,
};
// хелпер который позволит правильно позицинировать модельку при ресайзе экран
resize = () => {};
// просто матрица по умолчанию
#cameraMatrix = mat4.create();
// создал матрицу для мировой камеры, которая при каждом изменении записывала текущие расчеты матрицы.
cameraMatrix = new Matrix(() => {
this.#cameraMatrix = this.cameraMatrix.getCurrent();
});
// вставляем канвас в контейнер, выставляем камеру по значением которые подбирал вручную
init() {
const gl = this.#gl;
this.holder.appendChild(this.canvas);
this.cameraMatrix.setOrtho(0, 70, 70, 0, 120, -120);
this.resize() // важный момент, который поможет правильно выстроить канвас и прочее
// записываем значения в переменные которые получил из meshParams
const { uLineColorLocation, uBgColorLocation, uLineWidthLocation } =
this.#uniforms;
gl.uniform3fv(uLineColorLocation, this.meshParams.lineColor);
gl.uniform3fv(uBgColorLocation, this.meshParams.bgColor);
gl.uniform1f(uLineWidthLocation, this.meshParams.lineWidth);
}
/**
* @param {Mesh} mesh
*/
#renderMesh = (mesh) => {
const gl = this.#gl;
// ссылки на атрибуты
const { position, textureCoords } = this.#attrs;
// включаем буфер и записываем буфер фигуры в атрибут
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.positionsBuffer);
gl.vertexAttribPointer(position, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.uvBuffer);
gl.vertexAttribPointer(textureCoords, 2, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(
this.#uniforms.meshMatrixLocation,
false,
mesh.matrix.getCurrent()
);
// РИСУЕМ!
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
};
render() {
const gl = this.#gl;
// перед каждым рендером, очищаем полотно от всего.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// на всякий случай всегда прокидываю актуальные значение, если произошел ресайз окна
gl.uniformMatrix4fv(
this.#uniforms.uCameraMatrixLocation,
false,
this.#cameraMatrix
);
// получил объект с фигурами
const meshes = this.currentModel();
// на каждую фигуры вызвал рендер фигуры
Object.values(meshes).forEach(this.#renderMesh);
}
}
Фух! Как много кода, честно говоря даже писать комментарий к каждому моменту устал.Так, принцип работы получалсят такой:
- Загружаем модель .obj. Просто нужно получить доступ к его содержимому.
- Парсим содержимое с помощью функции который мне показали в конце 3 статьи.
- Получаем из этого массив вершин и faces которые поделены на части групп, а потом эти faces трансформируем в текстурные координаты.
- Прокидываем в класс ModelRender, он там сам все инициализирует в Mesh. Мб это не правильно? Наверно я должен был сам инициализировать Меши, а потом прокидывать их в рендер? Эх, поздняк метаться.
- Прокидываем настройки для модели, а потом меняем матрицу в моделе.
- Вызываем app.render()
- И вот у нас отрендеренная модель!
Для анимации, повторять 5-6 шаг до бесконечности.
let app = new ModelRender();
app.models = parsedModels; // объект с распарсенными моделями, { apple: { apple: { vertexes: [], uv: [] } }. Почему 2 apple? Потому, что код не учитывает, что у модельки не могут быть вложенных моделей.
app.holder = document.querySelector("#place");
app.modelName = "apple"; // указываем какую модель нужно рендерить
app.meshParams = appleParams; // цвет, толщина линии
app.init();
app.render(); // рендер с параметрами по умолчанию.
const model = app.currentModel();
const props = { x: 0 };
// Тут будет вся анимация.
anime({
targets: props,
easing: "linear",
loop: true,
x: { value: [0, 360], duration: 7e3 },
update() {
model.apple.setXRotate(props.x);
app.render();
},
});
С горем пополам написал рабочий примерИзвините, данный ресурс не поддреживается. :( Вещи про которые я еще узнал когда, делал рендер
- IE11 почти полностью поддерживает webgl, но там, чтоб обратиться к контексту нужно canvas.getContext("experimental-webgl"), достаточно один раз его запросить и больше париться не надо.
- Так же в ie11 или для safari нужно указывать версию шейдеров, чтоб нормально работало: #version 100
- Мне нужно было очищать память при переходе между страницами, для этого у webgl есть экстеншен loseContext, который позволяет сбрасывать все буферы и прочее.
- Модель яблока экспортирована без перевернутой оси Y и поэтому яблоко по умолчанию вверх ногами. Другие модели дизайнер экспортировал с перевернутой осью Y и поэтому, там сразу же моделька правильно вставала.
- На мобилках webgl выгружаться из памяти автоматически, поэтому нужно подписываться на эвент:
app.canvas.addEventListener("webglcontextlost", () => {
// your restore logic
console.log("restore");
});
В общем то и все, это было очень сложно, мне помогало очень много людей. Когда я вернулся спустя полтора года к статье, я вообще ничего не мог понять. Но все же, фронт может справиться с webgl, если захочет. Пока писал статьи, упустил наверно много моментов, но я старался выписать как можно больше моментов которые меня зацепили.Спасибо всем, кто прочитал и всем кто тогда помог написать данный рендер, а также тем кто помог почистить от лишнего статью.P.S. на конечный результат моих трудов можно посмотреть: https://digitalhorizon.vc/ (Это же не реклама?), посмотрите на все страницы кроме media и попробуйте перейти по плашкам с главной страницы на внутренние. Туда, я тоже запихнул webgl.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, WebGL] Знакомство фронтендера с WebGL: четкие линии (часть 3)
- [JavaScript, WebGL] Знакомство фронтендера с WebGL: первые наброски (часть 2)
- Выпуск jsii 1.31, генератора кода C#, Go, Java и Python из TypeScript
- [JavaScript, WebGL] Знакомство фронтендера с WegGL (часть 1)
- [JavaScript] DTO в JS
- [JavaScript, Разработка мобильных приложений] Разработка под iOS без Xcode
- [JavaScript] Как я писал тестовое задание на Angular и почему некоторым разработчикам не стоит давать тестовое задание
- [JavaScript, Node.JS] Создаем свой сайт или блог на Ghost в образе Docker
- [JavaScript, Алгоритмы] Быстрая математика для графиков, на примере вычисления среднего
- [JavaScript] Тест библиотек построения диаграмм классов, исследуя исходный код популярных js библиотек
Теги для поиска: #_javascript, #_webgl, #_webgl, #_javascript, #_vanilla (ванилла), #_frontend (фронтенд), #_render (рендер), #_javascript, #_webgl
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:03
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Это история в несколько частей:
Во-первых, это должно быть обязательно на классах! Почему на классах? Синтаксис красивый, а еще пригодится много контекста. Во-вторых, я буду использовать для анимации animejs, легкая библиотека для анимации с удобным апи (для меня). В-третьих, я буду парсить и кешировать модель загруженную модель в indexedDb (мне тогда казалось это крутой идеей и оптимизацией, а так же я просто хотел воспользоваться этим апи).Написанной мной шейдер был универсальным, ему было все равно какую модель ему загрузить, главное чтоб faces (индексы) состояли из квадратов, потому что если будут из каких-нить треугольников или пятиугольников, то весь мой парсер и шейдер полетит к чертям.По песочнице можно было понять, что вопроса как именно смещать фигуру для анимации у меня не было. Я просто высчитываю матрицу и загружаю ее в вершинный шейдер, а он уже сам все там рассчитывает и высчитывает точки.Как вообще выглядит анимация в webgl?
import { glMatrix, mat4, vec3 } from "gl-matrix"; // либа которая поможет складывать
export class Matrix { // я решил добавить коллбек который будет вызываться каждый раз когда какой-то метод в классе вызывали. constructor(onUpdate = () => {}) {} // объект с матрицами, по сути кеш и поможет в правильном порядке собирать матрицы, а это очень важно! #matrices = {}; // настройка камеры, до сих пор не разобрался как это работает, но позволяет разместить фигуру на нужной дистанции. setOrtho(left, right, bottom, top, near, far) {} // значение по умолчанию #scale = 1; // сеттер для управление scale setScale(ratio) {} // решил сделать геттор для получение актуального значения, // чтоб в будущем использовать для всяких рассчетов с анимацией. getScale() {} #translate = [0, 0, 0]; /** * x, y, z смещение * @param {[number, number, number]}params */ setTranslate(params) {} getTranslate() {} // ну и rotate, не объединял в один метод, потому, собирался анимировать каждое это значение отдельно. // если мне важно было знать для реализации translate и scale, то на rotate было все равно, поэтому геттеров не делал. Зачем? setRotateX(deg) {} setRotateY(deg) {} setRotateZ(deg) {} // перемножаем все матрицы и получаем итоговую которую нужно прокинуть в шейдер getCurrent() {} } import { Matrix } from "./Matrix";
export class Mesh { /** * * @param {Float32Array} positions * @param {Float32Array} uv */ // получаем вершины и текстурные координаты constructor({ positions, uv }) { this.positions = positions; this.uv = uv; // указываем drawArrays сколько у нас треугольников, так как для треугольника всегда нужно 3 точки, то можем смело делить массив с точками на 3 и получим итоговое значение треугольников this.count = this.positions.length / 3; // личная матрица для модельки this.matrix = new Matrix(); } // ссылочки на буфера positionsBuffer; uvBuffer; /** * @param {WebGLRenderingContext} gl */ // придумал для себя такой способ как прокидывать контекст для модельки, мне хотелось чтоб нужные классы моделей были уже созданы, их нужно только прокинуть в класс рендера. attachRender(gl) { this.gl = gl; } /** * Создаем буфера, загружаем в них данные, храним эти буферы потом в классе, чтоб легко достать */ initializeBuffers() {} } // вершинный шейдер
precision mediump float; uniform mat4 uMeshMatrix; // смещение модели uniform mat4 uCameraMatrix; // мировая камера attribute vec4 aPosition; // вершины attribute vec2 aTextureCoords; // текстурные координаты varying vec2 vTextureCoords; // интерполированные переменные, // на каждый пиксель между вершинами передаются интерполированные значения текстурных координат в фрагментный шейдер void main(){ gl_Position = uCameraMatrix * uMeshMatrix * aPosition; vTextureCoords = aTextureCoords; } // фрагментный шейдер
#extension GL_OES_standard_derivatives : enable // включаем штуку которая делает наши линии красивыми precision mediump float; // выставляем как нужно округлять float значения uniform vec3 uLineColor; // цвет линии uniform vec3 uBgColor; // цвет фона uniform float uLineWidth; // ширина линии varying vec2 vTextureCoords; // текстурные координаты которые нужны чтоб высчитать грани // моя супер логика для расчета границ // я не могу вспомнить как я это считал, кажется полтора года назад я был умней. float border(vec2 uv, float uLineWidth, vec2 gap) { // переменная gap нужна была, чтоб сделать линии более плавными, она рассчитывается динамически на основе текстурных координат благодаря директиве // smoothstep получал точка A и точку B, а потом получал какое-то значение и на основе этого значения возвращал плавное соотношение между А И Б. vec2 xy0 = smoothstep(vec2(uLineWidth) - gap, vec2(uLineWidth) + gap, uv); vec2 xy1 = smoothstep(vec2(1. - uLineWidth) - gap, vec2(1. - uLineWidth) + gap, uv); vec2 xy = xy0 - xy1; return clamp(xy.x * xy.y, 0., 1.); } void main() { vec2 uv = vTextureCoords; vec2 fw = vec2(uLineWidth + 0.05); #ifdef GL_OES_standard_derivatives // прогрессивное улучшение, на случай если директива не работает fw = fwidth(uv); #endif // получаем коэффициент который потом используем чтоб красить в нужным цветом. float br = border(vTextureCoords, uLineWidth, fw); // mix смешивает цвета, если значение 1 вернет полный цвет uLineColor, если 0, то вернет uBgColor. // А если значение где-то посередине, то вернет какой-то общий цвет между ними двумя. // ВИВА МАТЕМАТИКА gl_FragColor = vec4(mix(uLineColor, uBgColor, br), 1.); } import { vertex, fragment } from "./shaders"; // никакой магии, там просто шейдеры в строках
import { Mesh } from "./Mesh"; import { Matrix } from "./Matrix"; import { createProgramFromTexts, resizeCanvasToDisplaySize, saveRectAspectRatio, } from "./helpers"; export class ModelRender { // я предпочел чтоб класс сам создавал себе канвас, ему нужно просто указать куда его вставлять canvas = document.createElement("canvas"); // получаем контекст, тут еще полифил для ie11 #gl = this.canvas.getContext("webgl"); // Получаем экстеншен, чтоб дальше его включать. // Тут возвращается номер, который потом используется чтоб понять какой экстеншен нужно включить. #derivatives = this.#gl.getExtension("OES_standard_derivatives"); // Создаем программу из любимых шейдеров #program = createProgramFromTexts(this.#gl, vertex, fragment); // удобные мапы для работы со ссылками на атрибуты #attrs = { position: this.#gl.getAttribLocation(this.#program, "aPosition"), textureCoords: this.#gl.getAttribLocation(this.#program, "aTextureCoords"), }; // мапа для работы ссылками на переменные #uniforms = { meshMatrixLocation: this.#gl.getUniformLocation( this.#program, "uMeshMatrix" ), ... }; constructor() { const gl = this.#gl; // говорим карте проверять уровень треугольников, чтоб рисовалось только то что на переднем фоне gl.enable(gl.DEPTH_TEST); // загружаем программу в карту gl.useProgram(this.#program); // включаем атрибуты, ВСЕ НУЖНО ВКЛЮЧАТЬ gl.enableVertexAttribArray(this.#attrs.position); gl.enableVertexAttribArray(this.#attrs.textureCoords); } meshesByType = {}; models = {}; // я загружал сразу все модели в класс, а потом по ключу инициализировал нужную #initializeModel = (type) => {}; // `div` элемент в который вставят `canvas` /** @type {HTMLElement} */ holder; #modelName; // мне показалось прикольным переключаться между модельками с помощью сеттера set modelName(type) {} /** * Сделал геттер, который возвращал текущую модель. Нужно, чтоб была возможность анимировать модельку. Менять ей состояние матрицы. * @returns {Object<string, Mesh>} */ get currentModel() { return this.meshesByType[this.#modelName] } // объект из которого читались цвет фона и линии, а так же толщина // я заранее знал на какой странице какие цвета должны быть, поэтому не выносил этот объект в Mesh. meshParams = { bgColor: 0, lineColor: 0, lineWidth: 0.01, }; // хелпер который позволит правильно позицинировать модельку при ресайзе экран resize = () => {}; // просто матрица по умолчанию #cameraMatrix = mat4.create(); // создал матрицу для мировой камеры, которая при каждом изменении записывала текущие расчеты матрицы. cameraMatrix = new Matrix(() => { this.#cameraMatrix = this.cameraMatrix.getCurrent(); }); // вставляем канвас в контейнер, выставляем камеру по значением которые подбирал вручную init() { const gl = this.#gl; this.holder.appendChild(this.canvas); this.cameraMatrix.setOrtho(0, 70, 70, 0, 120, -120); this.resize() // важный момент, который поможет правильно выстроить канвас и прочее // записываем значения в переменные которые получил из meshParams const { uLineColorLocation, uBgColorLocation, uLineWidthLocation } = this.#uniforms; gl.uniform3fv(uLineColorLocation, this.meshParams.lineColor); gl.uniform3fv(uBgColorLocation, this.meshParams.bgColor); gl.uniform1f(uLineWidthLocation, this.meshParams.lineWidth); } /** * @param {Mesh} mesh */ #renderMesh = (mesh) => { const gl = this.#gl; // ссылки на атрибуты const { position, textureCoords } = this.#attrs; // включаем буфер и записываем буфер фигуры в атрибут gl.bindBuffer(gl.ARRAY_BUFFER, mesh.positionsBuffer); gl.vertexAttribPointer(position, 3, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, mesh.uvBuffer); gl.vertexAttribPointer(textureCoords, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv( this.#uniforms.meshMatrixLocation, false, mesh.matrix.getCurrent() ); // РИСУЕМ! gl.drawArrays(gl.TRIANGLES, 0, mesh.count); }; render() { const gl = this.#gl; // перед каждым рендером, очищаем полотно от всего. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // на всякий случай всегда прокидываю актуальные значение, если произошел ресайз окна gl.uniformMatrix4fv( this.#uniforms.uCameraMatrixLocation, false, this.#cameraMatrix ); // получил объект с фигурами const meshes = this.currentModel(); // на каждую фигуры вызвал рендер фигуры Object.values(meshes).forEach(this.#renderMesh); } }
let app = new ModelRender();
app.models = parsedModels; // объект с распарсенными моделями, { apple: { apple: { vertexes: [], uv: [] } }. Почему 2 apple? Потому, что код не учитывает, что у модельки не могут быть вложенных моделей. app.holder = document.querySelector("#place"); app.modelName = "apple"; // указываем какую модель нужно рендерить app.meshParams = appleParams; // цвет, толщина линии app.init(); app.render(); // рендер с параметрами по умолчанию. const model = app.currentModel(); const props = { x: 0 }; // Тут будет вся анимация. anime({ targets: props, easing: "linear", loop: true, x: { value: [0, 360], duration: 7e3 }, update() { model.apple.setXRotate(props.x); app.render(); }, });
app.canvas.addEventListener("webglcontextlost", () => {
// your restore logic console.log("restore"); }); =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:03
Часовой пояс: UTC + 5