Русский
Русский
English
Статистика
Реклама

Webgl

Перевод Визуализация списка женщин-лауреатов Нобелевской премии в виде кристаллов в 3d с использованием Vue, WebGL, three.js

13.06.2020 22:23:27 | Автор: admin
image

Год 1 | вдохновение


В этом месяце я очень долго пыталась определиться с датасетом и идеей для его обработки. Хотя я начала думать о нем еще в мае, по факту законить удалось только через 8 месяцев (черт, я плоха), а описать проект мне удалось еще спустя месяц (оу, я чертовски плоха).

Идея проекта пришла ко мне после просмотра фильма Безумно богатые азиаты. Мне очень понравилась актриса Мишель Йео, но идея оформилась только после того, как я прочитала больше о ней и узнала, насколько она была выдающейся и крутой. Это заставило меня задуматься выдающихся женщинах, о которых я понятия не имею. И вот возникла идея как-то это визуализировать.


1 неделя | данные


После формирования концепции, возник следующий вопрос как собрать необходимые данные? Я быстро пришла к идее использовать Википедию, попытаться спарсить лучших женщин оттуда. Я понятия не имела, как определить лучших (просмотры страницы? длина страницы? ссылки на страницу?). Хотя я посмотрела статью на The Pudding про визуализацию на основе данных википедии, я все еще не была уверена, что хочу двигаться в эту сторону. А еще мне очень не хватало свободного времени.

Затем мне пришло в голову, что, возможно, я должна использовать фиксированный список женщин, и это каким-то образом привело меня на страницу 51 женщин-лауреатов нобелевской премии. Я не слышала о большинстве из них, и мне захотелось узнать больше. Для проекта это подходило идеально.

image

С этого момента я начала изучать разные методы использования API Википедии. Разобраться было непросто (я не была уверена, нужно ли мне использовать Wikidata или MediaWiki, это те варианты, которые возникали при поиске Wikipedia API в Google), но, к счастью, уже упомянутая выше статья на Pudding помогла мне закрыть эти вопросы. Быстрый просмотр их репозитория wiki-billboard-data привел меня к выбору одного из скриптов с wikijs, и я могла просто воспользоваться этим пакетом.

Все, что мне нужно было сделать, это скопировать и вставить список знатных женщин-лауреатов в электронную таблицу и выполнить небольшое форматирование/чистку.

Один из самых важных уроков, которые я узнала о сборе данных электронные таблицы отлично подходят для их очистки. Раньше я пыталась писать код для очистки данных, и это занимало у меня намного больше времени. Когда я собирала данные для проекта по Гамильтону, я буквально набирала запятые между каждой ячейкой в строке в моем CSV настолько я была глупа.


Затем я экспортировала электронную таблицу как CSV, потом использовала онлайн-конвертер, чтобы получить данные в формате JSON.

После этого я написала простой скрипт, используя вики-страницы, чтобы получить больше информации о каждой женщине, включая количество ссылок на их страницу (обратных ссылок) и количество источников внизу, чтобы получить окончательный набор данных.

Несмотря на то, что создание идеи заняло целую вечность, сбор данных уложился всего в несколько часов.

неделя 2 | скетч


Не буду врать, я так долго думала об этом проекте, что даже не могу вспомнить все идеи, которые у меня возникали в процессе. Все, что я помню, это то, что я просматривала номинантов премии Information is Beautiful, когда решила, что мне нужно как-то выделить те записи, которые мне действительно нравятся в моих сохранениях. Это заставило меня осознать, что я должна лучше организовать свою доску на Pinterest, так как раньше я сваливала все мои идеи и видение на одну доску. В процессе чистки, я наткнулась на эту великолепную картину с кристаллами художника Ребекки Шаперон, которую я сохранила на будущее несколько лет назад. Почти сразу я поняла, что я хочу программно воссоздать их. Ведь было бы прекрасно визуализировать этих выдающихся женщин в качестве ярких разноцветных кристаллов?

image

Возможно, у меня были другие идеи до этого момента (да, очень возможно), но я не могу их вспомнить идея с кристаллами подходила идеально.

После этого я продумала другие детали: размер кристаллов должен отражать влияние женщины количество ссылок на ее страницу в Википедии. Число граней на кристалле будет сопоставлено с количеством источников в нижней части ее страницы (поскольку она многогранна), а цвет будет определяться категорией премии. Единственное, что мне пока было непонятно, это как расположить кристаллы. Я долго думала, что мне надо просто выложить их в 2-х измерениях по x / y и заставить читателя прокручивать их.

А потом я приняла участие в семинаре Мэтта ДесЛориерса по креативному кодингу, где он преподавал canvas, three.js и WebGL. Семинар открыл мне концепт третьего измерения, и я сразу поняла, что собираюсь использовать год получения награды в качестве оси z. Далее один мой друг предложил сделать потоки-нити, связывающие тех, кто сотрудничал друг с другом таким образом, чтобы их положение тоже менялось в зависимости от этого (но в итоге у меня не было времени на реализацию этой идеи).

Вся концепция пришла ко мне так быстро и естественно, что я не сделала ни одного наброска в процессе. Я просмотрела свой блокнот, но там вообще ничего нет.

неделя 3 & 4 | кодинг


Это был отличный месяц для кодинга.

В течение последних нескольких лет я хотела создать что-то с физическим воплощением, а в прошлом году я наконец решила сделать это моей целью. Но большую часть года я не знала, что это будет. Каждый раз, когда я размышляла об этом, я просто зацикливалась на двухмерных проекциях на стенах и не знала, как использовать весь объем.

И вот однажды меня осенило (я не уверена, что послужило причиной), что я не умею думать в трехмерном физическом пространстве, как раз потому, что я постоянно работаю в плоском диджитальном формате. Если бы я смогла научиться работать с 3D в цифре, то смогла бы мыслить в объеме и в физическом мире. Я добавила three.js и WebGL вверх своего списка для изучения.

В конце октября я прошла семинар Мэтта по креативному кодингу и изучила основы three.js, введение в фрагментированные и вершинные шейдеры. Я выучила правило буравчика: используйте большой палец для оси X (увеличивается вправо), указательный палец для оси Y (увеличивается вверх, что противоположно SVG и canvas) и средний палец для оси Z (для увеличения экрана и приближения его). Я узнала, что WebGL работает не в пикселях, а в единицах (похожая ассоциация футы или метры).

Затем в ноябре я вписалась в один конкурс по WebGL. Хотя я раньше не имела с ним дела, я надеялась, что дедлайн даст мне мотивацию, необходимую для завершения проекта. Я начала 1-го декабря и поставила себе цель делать чуть-чуть каждый будний день, пока не смогу дойти до чего-то презентабельного 23-го декабря (финал конкурса).

Сперва я прочла первые две главы Руководства по программированию на WebGL, в котором рассказывалось, как настроить WebGL. Затем я повторила свои знания по three.js. После я запустила блокнот из Observable, чтобы понять минимально-необходимые настройки для рисования чего-либо в three.js (рендерер, камера и сцена, а затем вызвать renderer.render(scene, camera) для отображения ). Мне всегда нравилось понимать самые основы работы кода, так что это было очень полезно.

После настройки я решила создать форму кристалла. Я использовала PolyhedronGeometry, потому что так я могла просто определить набор вершин, а затем указать вершины, которые будут составлять треугольную грань. В первый день мне удалось создать только один треугольник.

image

А во второй день кристалл (на который потребовалось две попытки, потому что в первый раз я налажала с математикой).

image

А потом, в конце концов, форма кристалла, который был у меня в голове.

image

Позже я поняла, что есть лучшие способы делать то, что я хотела (и на самом деле PolyhedronGeometry это довольно утомительный способ делать это), но я была очень рада возможности попрактиковаться в координатах x / y / z в WebGL.

Как только я была удовлетворена созданными формами, я перешла к изучению их окрашивания в цвета. Для этого я начала играться с вершинными и фрагментными шейдерами.

image

Следующей задачей стало научиться использовать фрагментный шейдер для окрашивания кристалла. Здесь мне пришлось немного отклониться от плана, потому что я не могла понять, как использовать glslify (узловая модульная система для GLSL, на которой написаны шейдеры, которые я взяла в качестве образца). Вместо этого я начала изучать различные инструменты компоновки/сборки, чтобы в конечном итоге развернуть свой код в Интернете. В конце концов, я решила использовать Parcel (вместо Vue CLI, который я регулярно использую последние полгода), потому что он имеет встроенную поддержку как Vue, так и GLSL.

Вот такой кристалл с наложением того же паттерна шума у меня получился.

image

Результат мне не нравился, поэтому я решила, что мне нужно больше узнать о шейдерах и использовании цветов в шейдерах. Именно в этот момент я обратилась к Books of Shaders Патрисио Гонсалеса Виво и, в частности, к главе Shaping Functions. Я узнала о синусах, косинусах, полиномах и экспонентах функциях, которые могут брать число (или набор чисел) и выводить на их основе другие. Я также узнала о смешивании цветов и о том, как мы можем взять два цвета, и не только получить новый цвет посередине между этими двумя, но также смешивать цвета в формате RGB и создавать совершенно новые на их основе. Если мы объединим это с функциями формы, то сможем получить градиенты и формы:

image
Эти были получены путем смешивания синего и желтого, а также изменения значений RGB в каждой позиции с помощью степеней, синусов, абсолютных значений, шагов и сглаживаний

Как только я была удовлетворена получившимися цветами, я переключилась на работу с данными: количество ссылок в нижней части для количества сторон, количество обратных ссылок для размера, категории Нобелевской премии для цвета.

image

Но результат мне не понравился по двум причинам:
  • Я жестко задавала формы каждой фигуры, поэтому у меня было только три разных фигуры: прямоугольник, прямоугольник с треугольником сверху и прямоугольник с треугольником сверху и снизу. Это означало, что большинство кристаллов были просто прямоугольниками.
  • Я также хотела, чтобы цвет для каждой категории выбирался из шести базовых цветов, поэтому для каждого кристалла я использовала только один цвет. Цвета мне совсем не нравились.


Примерно в это же время я наткнулась на демо Bloom (эффект постобработки для создания свечения), и в нем были круглые, похожие на драгоценные камни объекты, которые выглядели довольно похоже на кристаллы, которые я и хотела получить:

image

Я посмотрела исходный код и поняла, что они использовали SphereGeometry, но с включенным flatShading поэтому вместо рендеринга гладкой поверхности он показывал каждую отдельную грань. Я осознала, что, как я могу манипулировать и смешивать цвета, чтобы получить новые цвета, совершенно отличные от оригинала, так же я могу фактически управлять геометрическими параметрами (например, количеством граней) и получать новую геометрию, которая выглядит аналогично, но отличается от исходной.

Поэтому я заменила PolyhedronGeometry на SphereGeometry, установила высоту на 4 и ширину на свои данные, растянула фигуру, установив вертикальный масштаб в два раза больше по горизонтали, добавила jitter (дрожание) для каждой вершины, и у меня получились гораздо более интересные формы:

image

Теперь, когда я определилась с формами, настало время вернуться к цвету. На этот раз я использовала два цвета и смешивала их с функциями формы:

image
image
Мне нравится первый вариант выглядит как картофель

Поскольку шейдер по умолчанию просто обтекает фигуру (по крайней мере, так мне нравится думать об этом), я потеряла четкие края. К счастью, меня научили возвращать формы обратно, вызывая computeFlatVertexNormals() на геометрию, получая нормали в вершинном шейдере и передавая его фрагментному шейдеру при этом добавляя его в цвет. Это не только сделало края четко очерченными, но также дало иллюзию света и тени:

image
Код для добавления нормалей сторон.

После этого я поигралась с двумя наборами градиентов: один для гуманитарных наук (мир, литература, экономические науки), а другой для естественных наук (физика, химия, медицина).

image

Затем занялась фоном. Я создала пол с помощью PlaneGeometry и случайного дрожания y-позиции каждой вершины (вдохновленной этой статьей из codrops), а также небо, создав огромную сферу вокруг сцены. Я экспериментировала с тремя различными типами источников света: полусферой, рассеянным (чтобы создать длянеба ощущение заката/восхода солнца), и направленным (чтобы отбрасывать тени от кристаллов на пол).

image

В конце я добавила звездочки, которые визуализируют всех мужчин, которые выиграли премию за тот же период времени, а также аннотации для каждого кристалла и для десятилетий. Это было сложно, потому что с тем текстом, который у меня был, использовать TextGeometry было практически бесполезно. Решение, которое я нашла после поиска в интернете, визуализировать текст в HTML5 Canvas, создать PlaneGeometry и использовать этот Canvas в качестве текстуры для заполнения PlaneGeometry. Очень интересный подход.

Мне очень понравилось, что я смогла найти реальную причину использовать третье измерение десятилетия получения наград. Чем ближе они к переднему краю, тем более свежая по времени награда, и чем дальше они от нас, тем более старая эта награда. Но я не показываю эти десятилетия, пока пользователь не взлетит, чтобы посмотреть на кристаллы сверху. Если они прогуливаются между кристаллами на уровне земли, я показываю только информацию о каждой женщине, потому что я хочу, чтобы посетитель концентрировался только на женщинах во время прогулки.

image

Вот ссылка на финальный результат на GitHub

И видео c демонстрацией результата:



Если вы хотите попробовать еще что-то или начать с более простых проектов:

Подробнее..

Эффект параллакса в браузере с помощью TensorFlow.js WASM Three.js

29.06.2020 08:16:31 | Автор: admin
Помните как Apple представила iOS7 c эффектом параллакса? Сегодня я обьясню как я сделал это прямо в браузере.
image

На всех ноутбуках и телефонах сейчас есть камера, поэтому можно с помощью tensoflow моделей анализировать положения головы и глаз.



Наверно всем известно что есть библиотека Tensorflow для нейронных сетей, она работает под языком Python и Javascript. Процесс свертки в нейронных сетях это достаточно тяжеловесное вычисление, которое хорошо параллелится и существуют версии не только для CPU, но и на CUDA под Python и WebGL/WebGPU под Javascript. Забавно, но если у вас не NVidia, то оффициальная сборка Tensorflow на языке Javascript будет на ПК будет работать быстрее, так как нет официальной сборки с поддержкой OpenGL. Но к счастью для всех TF 2.0 имеет модульную архитектуру, которая позволяет думать только о самой сути, а не языке, на котором это выполняется. Так же существуют конвертеры 1.0 -> 2.0, которые я пока сам особо не пробовал.



На данный момент есть две официальные модели для распознования лица: facemesh и blazeface. Первая больше подходит для мимики и масок, вторая шустрее и просто определяет квадрат и характерные точки, такие как глаза, уши, рот. Поэтому я взял легковесный вариант blazeface. Зачем лишняя информация? Вообще может быть можно существующую модель еще уменьшить, так как кроме положения глаз, мне больше ничего не нужно.

В браузере на данный момент существует 5 бэкендов: cpu, wasm, webgl, wasm-simd, webgpu. Первый СPU слишком медленный и его сейчас не стоит брать ни в каких случаях, последний два слишком инновационны и находятся на стадии пропосалов и работают под флагами, так что поддержка у конечных клиентов нулевая. Поэтому я выбирал из двух: WebGL и WASM.



По существующим бенчмаркам можно увидеть, что что для маленьких моделей WASM работает порой быстрее чем WebGL. Кроме этого, параллакс может использоваться с 3д сценами и запуская WASM бэкенд на них, я понял что WASM работает гораздо лучше, так как дискретные видеокарты ноутбуков, не вывозят одновременно нейросети и 3д сцены. Для этого я взял простую сцену с 75-ю гловами в .glb. Ссылка кликабельная и там WASM.



Если вы перешли по ссылке, вы наверно заметили, что браузер спросил разрешение на доступ к видеокамере. Возникает вопрос: что будет, если пользователь нажал нет? Разумно было бы ничего не грузить в таком случае и фоллбэчить на управление мышью\гироскопом. Я не нашел ESM версию tfjs-core и tfjs-converter, так что вместо динамического импорта, я остановился на довольно криповой конструкции с бибилиотекой fetchInject, где порядок загрузки модулей имеет значение.
криповая конструкция
Забавно то, что порядок расположения имеет значение и нельзя их оборачивать в один массив (Promise.All), так как это приведет к довольно не приятной ошибке, которая исчезает после первой загрузки и дальше не воспроизводится, так как скрипты попадают в кэш.
fetchInject([  'https://cdn.jsdelivr.net/npm/@tensorflow-models/blazeface@0.0.5/dist/blazeface.min.js'], fetchInject([  'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-converter@2.0.1/dist/tf-converter.min.js',  'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@2.0.1/dist/tfjs-backend-wasm.wasm'], fetchInject([  'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core@2.0.1/dist/tf-core.min.js']))).then(() => {  tf.wasm.setWasmPath('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@2.0.1/dist/tf-backend-wasm.min.js');  //some other code}



Для того что бы найти положение взгляда, я беру среднее между двумя глазами. И легко понять что растояние до головы, будет пропорционально растоянию между глазами на видео, в силу подобия сторон треугольника. Данные о положении глаз, которые приходят из модели довольно зашумленные, поэтому прежде чем производить рачеты, я их сгладил с помощью скользящей средней EMA (Exponential Moving Average)

pos = (1 - smooth) * pos + smooth * nextPos;

Или записав по другому

pos *= 1 - smooth;pos += nextPos * smooth;


Таким образом мы получаем координаты x, y, z, в некой сферической системе координат с центром в камере. Причем x и y ограничены углом обзора камеры, а z это примерное растояние от головы до нее. На малых углах поворота $sin(\alpha) \approx \alpha$, так что x и y можно считать за углы.

pushUpdate({  x: (eyes[0] + eyes[2]) / video.width - 1,  y: 1 - (eyes[1] + eyes[3]) / video.width,  z: defautDist / dist});


Фотограмметрия



Довольно забавный дата сет с публикации SIGGRAPH 2020 Immersive light field video with a layered mesh representation. Они делали картинки специально для того что бы можно было двигать камеру в некотором диапазоне, что идеально ложится под идею параллакса. Пример с параллаксом.



3д сцена тут создается послойно, и на каждый слой наложена текстура. Не менее забавно выглядит дейвайс, с помощью которого они создавали фотограмметрию. Они купили 47 дешевых экш камер Yi 4K за 10к рублей каждая, и разместили их на полусферу в форме икосаэдра в котором треугольная сетка утроена. После этого все разместили на штатив и камеры синхронизировали для сьемки.



Ссылки:


Подробнее..
Категории: Javascript , Webgl , 3d , Tensorflow , Webassembly , Tfjs , Wasm , Three.js

Canvas и геометрия. Это почти просто

06.07.2020 14:13:22 | Автор: admin

Трехмерную графику можно реализовать в браузере не только применяя WebGL или
библиотеки созданные на основе WebGL, но и путем простой отрисовки на 2D-холсте используя
для этого функции HTML5 Canvas.


Существует множество книг, статей и информации в интернет о том как использовать холст
HTML5 Canvas для отображения рисунков, графиков и даже анимации в некоторых браузерных играх.
Однако практически никогда не рассматривается возможность непосредственного использования холста
для отображения трехмерных объектов.


Остановимся на этом чуть подробнее.

Если создается трехмерная компьютерная игра, например 3D-шутер, то разработчики стараются сделать ее
как можно более реалистичной и используют для этого графические ускорители. В этом случае без соответствующих
программных инструментов обойтись не удастся. Для таких игр в браузере есть прямой смысл
использовать WebGL и Three.js. Самое главное, что с помощью шейдеров предназначенных для этих
программных инструментов можно осуществить реалистичное освещение сцены и находящихся на сцене объектов.


Но если требуется просто показать трехмерный объект со всех сторон, то можно обойтись
без WebGL и шейдеров. Например некоторые программы CAD/CAM предназначены только для моделирования
формы объектов и не предполагают использования реалистичного освещения.
На сайте Canvas и геометрия
я последовательно и очень подробно объясняю как используя исключительно HTML5 Canvas можно
создать изображения 3D-моделей многогранников.




Полгода тому назад была опубликована моя статья Three.js и геометрия
которая относилась к серии из 30 глав (уроков) на одноименном сайте
Three.js и геометрия где
мной описывалось отображение 3D-моделей многогранников при помощи three.js. Теперь все модели
на новом сайте созданы только с помощью HTML5 Canvas. Однако сам расчет координат вершин моделей
на обеих сайтах сделан абсолютно одинаково. Отличие заключается лишь в способе вывода изображения на экран.


В принципе эти два сайта можно рассматривать как единый учебный курс в котором
рассматриваются два способа отображения одних и тех же моделей на холст с использованием библиотеки three.js
и отображение моделей на холст без использования этой библиотеки. Можно посмотреть как выглядит программа
отображения одного и того же многранника зайдя по очереди или одновременно на оба сайта.
Перечислю некоторые из тех возможностей которые доступны на обоих сайтах, но сделаны они на них по разному:



Можно задать практически все параметры модели определяющие размеры ее
отдельных частей, наклон граней модели и т.д.

Просмотр нумерации вершин модели.

Масштабирование модели.

Возможность выбора любой грани модели мышью
(при использовании three.js это делается с помощью RayCaster'а).

Возможность просмотра значений координат любой точки находящейся на поверхности модели.

Проверка корректности построенной модели.
При желании можно выключать/включать проверку корректности построения модели.

Показано как на 3D-модели отобразить размеры ее частей
при помощи выносных и размерных линий. На сайте сделанном при помощи three.js
эта возможность отсутствует.

Имитация простейшего освещения.

Задание цвета и яркости падающего на модель света.

Задание прозрачности модели.



Если используется WebGL/three.js программист может
не заботиться об удалении невидимых линий это
делается автоматически при помощи Z-буфера.
На сайте Canvas и геометрия
показано как можно отображать только внешние (направленные к наблюдателю) грани модели
двумя различными способами.


При создании online-программ находящихся на сайте использовались лишь самые простые конструкции языка JavaScript.
Самая сложная конструкция этого языка, которая используется на сайтах это prototype в моей библиотеке WebGeometry,
при помощи которой производятся геометрические расчеты. Хотя можно было бы в принципе при создании этой библиотеки обойтись и
без prototype. В самом начале моей работы по отображению трехмерных моделей в браузере мне было весьма непривычно
пользоваться JavaScript. Мне казалось диким, что в языке нет заранее прдопределенных типов данных. Да и некоторые
другие его особенности также казались весьма странными.
В течение многих лет я программировал на C/C++ и поэтому поначалу JavaScript мне показался ущербным языком.
Но спустя некоторое время до меня дошло, что имея только браузер (я в основном использую Chrome и иногда Firefox)
и Notepad++ работать на JavaScript очень удобно результат виден сразу особенно это относится к небольшим
графическим программам.


Если не относться заранее предвзято к особенностям языка, то он оказывается достаточно комфортен.
Поэтому сейчас я изменил свое отношение к этому языку в гораздо лучшую для него сторону.
Иногда правда я с тоской вспоминаю замечательный (это мое личное мнение) отладчик в Visual Studio.
Поэтому, когда требуется найти сложную ошибку в геометрических вычислениях,
я обращаюсь к Visul C++ и этому отладчику. Но отладчики в Chrome и Firefox тоже
производят самое хорошее впечатление.


Сейчас для создания больших программ на JavaScript все время появляются новые инструменты такие как React,
Angular, Vue.
Освоение этих программ требует значительных усилий. К счастью для создания простых графических программ можно обойтись
без них. У меня даже пока не возникала потребность в использовании новых (на данный момент уже и не новых) возможностей ES6.


Однако для понимания работы этих программ требуется некоторый опыт в использовании обработчиков событий мыши возникающих
при ее движении по холсту и при нажатии ее клавиш.
Все online-программы сделаны максимально интерактивными. При помощи мыши можно не только выбрать
любую грань модели, но также посмотреть значения координат модели в любой ее точке, координаты точек на самом холсте,
вращать модель и (на одной странице сайта) осуществлять масштабирование модели колесиком мыши.



Для отрисовки моделей и нанесения их размеров на холст была сделана библиотека
canvas2D,
которая фактически представляет собой небольшую обертку вокруг функций HTML5 Canvas.
При этом функции входящие в библиотеку canvas2D сделаны таким образом, чтобы их было
максимально удобно использовать совместно с функциями входящими в состав библиотеки
WebGeometry

,
предназначенной для геометрических расчетов на плоскости и в пространстве.

Несколько слов про использование библиотеки WebGeometry. Эта библиотека сделана таким
образом, чтобы программист котрый создает 3D-модель с нуля не задумывался о том как решить очередную задачу
аналитической геометрии на плоскости и в пространстве для прямых и плоскостей. В основном для решения таких
задач требуются неплохие знания работы с матрицами, и если у вас нет соответствующих готовых функций
то перед вами возникают дополнительные проблемы. В библиотеке WebGeometry нет функций
для перспективных преобразований в пространстве, которые производятся при помощи не трехмерных, а четырехмерных матриц.
Перспективные преобразования не требуются при создании трехмерных моделей. Поэтому они и не включены
в состав библиотеки WebGeometry. Если потребуется выполнить такие преобразования, то соответствующие
функции можно найти, если вы используете three.js, то в самой этой библиотеке или в популярной библиотеке
glMatrix

.
Однако в этих двух упомянутых библиотеках практически нет функций для решения задач аналитической геометрии.
Поэтому и была сделана библиотека функций WebGeometry. В библиотеке
Sylvester есть некоторые (но не все требуемые)
такие функции и поэтому я решил сделать библиотеку полностью подходящую под мои задачи с нуля
на основе уже существующих функций на C/C++.

Для себя я сделал вывод, что простую отрисовку трехмерных моделей без реалистичного освещения
на холсте можно сделать достаточно просто и значительно легче чем с использованием three.js.
На мой взгляд это можно сравнить как отрисовку трехмерной модели с помощью OpenGL с фиксированным
конвейером с отрисовкой той же модели и использованием OpenGL с программируемым конвейером.
Например, во втором случае придется делать триангуляцию многоугольников, что значительно
усложняет процесс программирования.



Тем кого заинтересует этот сайт, возможно, покажутся интересными и некторые
другие страницы моих сайтов связанные с отображением моделей на экране компьютера:



На сайте Three.js и геометрия все 16 моделей многогранников расчитаны с помощью библиотеки
WebGeometry (так же как и на текущем сайте), но отображены они на холст с помощью three.js.



На сайте Diamond Cuts модели 7 многогранников также расчитаны спомощью библиотеки
WebGeometry и отображены на холст с помощью three.js. При этом используется
несколько типов шейдеров и кубические текстуры. Все программы на этом сайте снабжены
подробными комментариями и поэтому на их примере можно посмотреть как
шейдеры встраиваются в three.js.



На страницах
Diamond Cuts collection in environments
и Diamond Cuts with Dispersion Light
можно найти множество моделей различных многогранников отображенных при помощи шейдеров и кубических текстур.
Вывод на экран моделей с этих страниц осуществляется при помощи three.js.
При определении координат вершин всех этих моделей координаты предварительно расчитывались на Visual C++
и их значения записывались в файлы. Эти файлы с сохраненными в них значениями координат используются
в программах отображения моделей на экран (three.js и API WebGL).

По ссылке
Pages можно перейти на страницы этого сайта на которых находятся 36 моделей различных огранок.
Вывод на экран на этих страницах осуществляется при помощи чистого API WebGL.
Также отсюда можно скачать exe-файл моей программы под Windows сделаной на C/C++
и OpenGL для просмотра моделей огранок. DLL-файлы нескольких моделей скачиваются с этой же страницы.

На страницах
Shine on You Crazy Diamond ! того же сайта можно под музыку Pink Floyd и других музыкальных групп
двигаться по сцене и рассматривать модели огранок. Управление движением мышь и клавиатура (, , , , <, > )
как в компьютерных играх. Вывод на экран осуществляется при помощи three.js.
На мой взгляд получилось весьма интересно. Не забудьте при прсмотре включить звук!



Надеюсь, что мои сайты окажут помощь изучающим и использующим трехмерную графику и вычислительную
геометрию. Кроме просмотра сайтов можете зайти в
мой репозиторий.

Спасибо всем за просмотр моей статьи!

Подробнее..

Опыт создания нескольких 3д сцен без перезагрузки страницы (three.js)

14.09.2020 12:17:21 | Автор: admin

Что делаю


Для личного проекта потребовалось найти решение, для работы с некоторым количеством разных gltf моделей.


Небольшой опыт работы с three.js у меня был, поэтому, после ознакомления с примерами GLTFLoader, была выбрана именно эта библиотека.


Данные о сценах хранятся в отдельном json файле и содержат информацию о пути к сцене, освещении, маршруте камеры и т.д. но в данном материале они нас интересуют в меньшей мере.


В теории было необходимо рисовать подряд несколько сцен. На практике все оказалось несколько сложней.


Как делаю


Камера


Первая трудность заключалась в построении маршрута для движения камеры. Из требований к
камере и передвижению:


  • Камера должна двигаться по ключевым точкам, достраивая промежуточные;
  • камера должна зависеть от скролла колеса мышки (передвигаясь вперед и назад, соответственно);
  • камера должна плавно тормозить и сглаживать свое движение.

Для построения и интерполяции пути подошел CatmullRomCurve3 с методом getPoint(), который строил достаточно плавную кривую. Остальные классы кривых, такие как Curve или CubicBezierCurve3 строили промежуточные точки не достаточно плавно. Это следует учитывать.


Привязка скролла мыши к движению камеры достаточно проста. Определив направление скролла, изменяем текущее положение глобальной переменной (прибавкой или вычетом дельты). Полученное значение делим на расстояние от начального положения камеры до ее центра (по иксу). Это связанно с тем, что в моем случае сцена начинается на некотором удалении от центра, а событие перезагрузки модели срабатывает, когда координата камеры (по иксу) ровна нулю. Решение не универсальное, но я придерживался такого подхода, поскольку именно так задумывал проект.


Дополнительная задача была связана и с вращением камеры. Для этого использовался TrackballControls с начальным фокусом в точке (0, 0, 0). При нажатии на кнопки управления (W, S, D, A в данном случае), фокус можно было, аналогично движению по кривой, смягчить (в обоих случая использовал дополнительные таймеры).


Память


Реализовав несколько подряд идущих сцен обратил внимание, что fps с каждой новой подгрузкой падает. Очевидно, где-то происходит утечка памяти. Делаю снапшоты для нескольких сцен. Подчищаю некоторые мелочи. Проверяю результат.




Не выглядит, как проблема. Однако, фпс продолжает падать. Использую диспетчер задач хрома, чтобы посмотреть, в чем проблема. Выясняется, что память GPU при перезагрузке сцен не очищается, а продолжает забиваться.


Посмотрев материалы на тему использования three.js в SPA (например от Дриеса Де Смета) выяснилось, что очевидный способ удаления элементов сцены не самый правильный.


for (let i = mScene.scene.children.length - 1; i >= 0; i--) {    mScene.scene.remove(mScene.scene.children[i]); // объекты пропадают, но остаются в памяти}

Более. Рекомендации к использованию метода очистки геометрии и текстур объекта весьма расплывчаты. Вот что пишут о методе dispose() в документации:


In general, there is no definite recommendation for this. It highly depends on the specific use case when calling dispose() is appropriate. It's important to highlight that it's not always necessary to dispose objects all the time. A good example for this is a game which consists of multiple levels.

Очевидно, что в данном проекте необходимо использовать dispose. Но как? Эмпирически пришел к следующему варианту (представлен в сокращенном виде):


dispose_scene() {    let self = this;    self.scroll_timer_stop();    this.scene.traverse(function (object) {    self.scroll_timer_stop();        if (object.type === "Mesh" || object.type === "Group") {            self.dispose_hierarchy(object, self.dispose_node);            self.scene.remove(object);            object = null;       }    });}dispose_hierarchy(node, callback) {    for (var i = node.children.length - 1; i >= 0; i--) {        var child = node.children[i];        this.dispose_hierarchy(child, callback);        callback(child);    }}dispose_node(node) {        if (node.constructor.name === "Mesh") {            node.parent = undefined;            if (node.geometry) {                node.geometry.dispose();            }            if (node.geometry) {                node.geometry.dispose();            }            let material = node.material;            if (material) {                if (material.map) {                    material.map.dispose();                }                if (material.lightMap) {                    material.lightMap.dispose();                }                ...                material.dispose();                material = undefined;            }        } else if (node.constructor.name === "Object3D") {            node.parent.remove(node);            node.parent = null;        }}dispose_postprocessing() {         this.postprocessing.rtTextureColors.dispose();        this.postprocessing.rtTextureDepth.dispose();        ...        this.postprocessing.materialGodraysDepthMask.dispose();        this.postprocessing.materialGodraysGenerate.dispose();        ...}

Итог


В итоге хотелось бы сказать, что текущие механизмы работы с памятью в three.js не очень прозрачные и очевидные. this.postprocessing.dispose() в предыдущем примере не так влияет на общий расход памяти, а вот пошаговое и методичное удаление всех составляющих, к которым применим dispose() приводит к тому, что ты перестаешь платить ресурсами за то, чего не видишь. Хотя, безусловно, комплексного решения для очистки я не нашел. Более. Нагрузка на видеокарту при работе страницы огромная. Geforce 2070 super выдает стабильный фпс и плавность анимаций, но демонстрирует следующую картину:




Был бы рад услышать ваши комментарии и опыт работы с аналогичными проектами. С исходным кодом и примерами можно ознакомиться в моем гитхабе. Спасибо за внимание!

Подробнее..

Продолжаем чистить память с three.js

30.09.2020 22:05:00 | Автор: admin

Введение

Недавно писал о своем опыте очистки памяти в приложении, с использованием three.js. Напомню, что целью была перерисовка нескольких сцен с подгрузкой gltf моделей.
С тех пор я провел ряд экспериментов и считаю необходимым дополнить сказанное ранее этой небольшой статьей. Вот некоторые моменты, которые помогли мне улучшить производительность приложения.

Основная часть

Изучая различные примеры сборки мусора на three.js заинтересовал подход, предложенный на threejsfundamentals.org. Однако, реализовав предложенную конфигурацию и завернув в this.track() все материалы и геометрию, выяснилось, что при загрузке новых сцен нагрузка на GPU продолжает расти. Более того, предложенный пример некорректно работает с EffectComposer и другими классами для постобработки, поскольку в этих классах track() использовать нельзя.
Решение с добавлением ResourceTracker во все используемые классы не привлекает, по очевидным причинам, поэтому решил дополнить метод очистки упомянутого класса. Вот некоторые приемы, которые были использованы:

Прием 1. Грубый.

Добавляем renderer.info после метода очистки. Поочередно убираем ресурсы из приложения, чтобы понять, какие из них составляют нагрузку и прячутся в текстурах или материалах. Это не способ решение проблем, а просто способ отладки о котором кто-то мог не знать.

Прием 2. Долгий.

Открыв код используемого класса (например AfterimagePass, который можно найти на гитхабе three.js) смотрим, где создаются ресурсы, которые нам нужно очищать, чтобы поддерживать число геометрий и материалов в требуемых рамках.
this.textureComp = new WebGLRenderTarget( window.innerWidth, window.innerHeight, { ... }
То что надо. Согласно документации, WebGLRenderTarget имеет функцию dispose, на которой завязана очистка памяти. Получаем что-то вроде
class Scene {//...    postprocessing_init(){ // В нашем классе        this.afterimagePass = new AfterimagePass(0);        this.composer.addPass(this.afterimagePass);    }//...}//...class ResourceTracker {//...    dispose() {    //...    sceneObject.afterimagePass.WebGLRenderTarget.dispose();    //...    }}


Прием 3.

Работает, но код для очистки в таком случае раздувается. Попробуем использовать знакомый нам из предыдущей статьи подход. Напомню, в ней мы реализовали метод disposeNode(node), в котором ресурс перебирался на поиск того, что можно очистить. disposeNode() может выглядеть как-то так:
disposeNode(node) {            node.parent = undefined;            if (node.geometry) {                node.geometry.dispose();            }            let material = node.material;            if (material) {                if (material.map) {                    material.map.dispose();                }                if (material.lightMap) {                    material.lightMap.dispose();                }                if (material.bumpMap) {                    material.bumpMap.dispose();                }                if (material.normalMap) {                    material.normalMap.dispose();                }                if (material.specularMap) {                    material.specularMap.dispose();                }                if (material.envMap) {                    material.envMap.dispose();                }                material.dispose();            }        } else if (node.constructor.name === "Object3D") {            node.parent.remove(node);            node.parent = undefined;        }    }

Отлично, теперь возьмем все дополнительные классы, которые мы применяли, и дополним наш ResourceTracker:
dispose() {    for (let key in sceneObject.afterimagePass) {        this.disposeNode(sceneObject.afterimagePass[key]);    }    for (let key in sceneObject.bloomPass) {        this.disposeNode(sceneObject.bloomPass[key]);    }    for (let key in sceneObject.composer) {        this.disposeNode(sceneObject.composer[key]);    }}


Итоги

В результате всех этих действий я значительно повысил ФПС и уменьшил нагрузку GPU в своем приложении. Возможно, я некорректно применял ResourceTracker, однако он в любом случае не помог бы в работе с дополнительными классами. Про то, что перебор EffectComposer через наш disposeNode(node) влияет на число текстур, оказывающихся в памяти я нигде не видел (однако так оно и есть). Этот вопрос следует рассмотреть отдельно.
Для сравнения предыдущая версия останется по старому адресу, а новую можно будет посмотреть отдельно. Проект в некотором виде есть на гитхабе.

Буду рад услышать ваш опыт по работе с аналогичными проектами и обсудить детали!
Подробнее..

Особенности масштабирования WebGL-карты

22.12.2020 08:17:45 | Автор: admin
Мывыпустили редактор стилей. Подробно о том, как сним можно настроить карту под задачи сервиса, можно почитать наvc.ru. НаХабреже хотим рассказать оконцепции StyleZoom, которую мыиспользуем втом числе ивредакторе стилей.

Под катом небольшой рассказ озум-левелах: почему встандартном виде они неподходят для больших территорий икак масштаб влияет назагрузку тайлов ивнешний вид карты.




Зум-левел


Для обозначения масштаба вWebGL-карте 2ГИС, как ивомногих других картах, используется число зум-левел или просто зум. Зум, равный нулю, соответствует масштабу карты, при котором весь мир помещается вквадрат размером 256256px.


Карта мира при zoom = 0

Увеличение зума наединицу соответствует растяжению карты вдва раза. Напервом зуме весь мир будет размером 256 2 = 512px. Начетвёртом получаем размер 256 2 2 2 2 = 4096px.

Такая система позволяет обозначать диапазон масштабов удобными для понимания числами. Например, zoom = 11 примерно один крупный город наэкране, zoom = 19позволяет детально рассмотреть здания ипроходы между ними.

Проекция Меркатора


Вкартах 2ГИС используется картографическая проекция Меркатора. Картографическая проекция способ отобразить сферическую поверхность Земли наплоской карте.

Так как плоскость ишар неодно итоже, любая картографическая проекция искажает форму или размеры объектов. Впроекции Меркатора объекты набольших широтах накарте выглядят больше, чем объекты наэкваторе. Поэтому натаких картах Гренландия выглядит размером сАфрику, хотя фактически еёплощадь меньше площади Африки в14раз. Здесь можно рассмотреть, как проекция искажает размеры стран.


Если Россию приблизить к экватору, её размер на карте значительно уменьшается

Растяжение объектов пропорционально 1 / cos(lat), где lat широта объекта. Из этой формулы следует, что объекты на широте Санкт-Петербурга (lat = 60) на карте будут растянуты в два раза. А объекты на Северном или Южном полюсах (lat = 90) и вовсе растянутся до бесконечности. Именно поэтому на картах в проекции Меркатора никогда не рисуются полюса на них обрезается всё севернее и южнее 85 широты.

Подробнее о проекции Меркатора можно почитать в этом наглядном и увлекательном материале.

Проблемы сзумом ипроекцией Меркатора


Изсвойств проекции Меркатора вытекает главная проблема сзумами: один итотже зум-левел наразных широтах соответствует разным фактическим масштабам карты.

Сравним скриншоты карты на14-м зуме вМурманске (широта69) иТашкенте (широта41).


15-й зум в Мурманске


15-й зум в Ташкенте

Видно, насколько сильно отличаются поразмеру дома. ВМурманске они крупные, вТашкентеже карта мельчит.

При создании стилей карты мыхотим придать ейопределённый внешний вид. Для этого задаём ширину дорог впикселях взависимости отзума или зум, скоторого начинаем показывать дома. Стили, написанные для одной широты, надругой будут выглядеть совсем иначе. Смысл, заложенный вних, исказится, так как зумы будут означать уже другой фактический масштаб.

Решение


Для решения этой проблемы мыввели понятие styleZoom, который может отличаться отобычного zoom.
Обычный zoom StyleZoom
Определяет масштабирование объектов Определяет, какие тайлы грузить
Записывается в url Определяет, для какого масштаба применять стили
Используется в привычных методах getZoom / setZoom Используется в методах getStyleZoom / setStyleZoom
Совпадает с растровыми тайлами

styleZoom вычисляется изzoom ишироты посдедующей формуле: styleZoom = zoom + log2(1/ (2 * cos(lat)).

Изформулы вытекают следующие свойства styleZoom:
  • Нашироте 60styleZoom = zoom. Так как стили изначально писались наНовосибирск иМоскву, решили взять эту широту забазовую.
  • Наширотах <60styleZoom < zoom. Наэкваторе styleZoom = zoom 1.
  • Наширотах >60 styleZoom > zoom.

Посмотрим теперь, как будут выглядеть Ташкент и Мурманск при styleZoom = 15.


Ташкент, styleZoom = 15 (zoom 15.59)


Мурманск, styleZoom = 15 (zoom 14.53)

Хорошо видно, что styleZoom соответствует реальному масштабу карты вне зависимости отшироты: дома визуально неотличаются поразмеру, карта вдвух разных городах выглядит одинаково.

Ограничения


Умеханизма коррекции есть недостатки, которые проявляются вдвух сценариях работы скартой.

zoom < 9
Нанебольших зумах, когда наэкране виден весь мир или его бльшая часть, перетаскивание карты вверх-вниз приводит кбольшим изменениям широтыи, соответственно, styleZoom.

Вовремя перетаскивания это может привести кзагрузке новых тайлов, переключению стилей, появлению или исчезновению объектов ит.п. Чтобы избежать этого эффекта, приzoom<9коррекция отключается, иstyleZoom приравнивается кzoom.

lat > 60
Наочень больших широтах styleZoom становится сильно больше zoom. Так как styleZoom отвечает зато, какие тайлы грузить, может оказаться, например, что на14-м зуме мызагрузим иотобразим тайлы 16-го зума. Тайл 16-го зума в16раз меньше поплощади, чем тайл 14-го зума. Иесли обычно наэкран попадает 30тайлов, товэтом случае ихбудет 480. Аколичество тайлов сильно влияет напроизводительность. Чтобы незагружать видеокарту наэтих широтах, при lat > 60коррекция отключается, иstyleZoom также приравнивается кzoom.

Вместо вывода


Подключайте API наших WebGL-карт иоформляйте картыпод задачи вашего сервиса, непереживая, как они будут выглядеть вразных городах.

Запост отдельное спасибо Лёше Федосову. Лёха, привет!
Подробнее..

Делаем свой minecraft на JavaScript

03.01.2021 20:22:24 | Автор: admin

Добро пожаловать в самую запутанную архитектуру проекта. Да я умею писать вступление...


image

Попробуем сделать небольшую демку minecraft в браузере. Пригодятся знания JS и three.js.

Немного условностей. Я не претендую на звание лучшее приложение столетия. Это всего лишь моя реализация для данной задачи. Также есть видео версия для тех кому лень читать(там тот же смысл, но другими словами).

Вот здесь видеоверсия


В конце статьи есть все нужные ссылки. Постараюсь как можно меньше воды в тексте. Объяснять работу каждой строки не буду. Вот теперь можно начать.

Для начала чтобы понимать какой будет итог, то вот демка игры.

Разделим статью на несколько частей:

  1. Структура проекта
  2. Игровой цикл
  3. Настройки игры
  4. Генерация карты
  5. Камера и управление


Структура проекта



Вот так выглядит структура проекта.
image

index.html Расположение канваса, немного интерфейса и подключение стилей, скриптов.
style.css Стили только для внешнего вида. Самое важное это кастомный курсор для игры который располагается в центре экрана.
texture Здесь лежат текстуры для курсора и блока земли для игры.
core.js Основной скрипт где происходит инициализация проекта.
perlin.js Это библиотека для шума Перлина.
PointerLockControls.js Камера от three.js.
controls.js Управление камерой и игроком.
generationMap.js Генерация мира.
three.module.js Сам three.js в виде модуля.
settings.js Настройки проекта.

index.html
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" href="style/style.css"><title>Minecraft clone</title></head><body><canvas id="game" tabindex="1"></canvas><div class="game-info"><div><span><b>WASD: </b>Передвижение</span><span><b>ЛКМ: </b> Поставить блок</span><span><b>ПКМ: </b> Удалить блок</span></div><hr><div id="debug"><span><b></b></span></div></div><div id="cursor"></div><script src="scripts/perlin.js"></script><script src="scripts/core.js" type="module"></script></body></html>

style.css
body {margin: 0px;width: 100vw;height: 100vh;}#game {width: 100%;height: 100%;display: block;}#game:focus {    outline: none;}.game-info {position: absolute;left: 1em;top: 1em;padding: 1em;background: rgba(0, 0, 0, 0.9);color: white;font-family: monospace;pointer-events: none;}.game-info span {display: block;}.game-info span b {font-size: 18px;}#cursor {width: 16px;height: 16px;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);background-image: url("../texture/cursor.png");background-repeat: no-repeat;background-size: 100%;filter: brightness(100);}

Игровой цикл


В core.js нужно провести инициализацию three.js, настроить его и добавить все нужные модули от игры + обработчики событий ну и игровой цикл запустить. В учет того, что все настройки стандартные, то объяснять их нет смысла. Поговорить можно про map(он принимает сцену игры для добавления блоков) и contorls т.к. он принимает несколько параметров. Первый это камера от three.js, сцену для добавления блоков и карту чтобы можно было взаимодействовать с ней. update отвечает за обновление камеры, GameLoop игровой цикл, render- стандарт от three.js для обновления кадра, событие resize также стандарт для работы с канвасом(это реализация адаптива)

core.js
import * as THREE from './components/three.module.js';import { PointerLockControls } from './components/PointerLockControls.js';import { Map } from "./components/generationMap.js";import { Controls } from "./components/controls.js";// стандартные настройки three.jsconst canvas= document.querySelector("#game");const scene = new THREE.Scene();scene.background = new THREE.Color(0x00ffff);scene.fog = new THREE.Fog(0x00ffff, 10, 650);const renderer = new THREE.WebGLRenderer({canvas});renderer.setSize(window.innerWidth, window.innerHeight);const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);camera.position.set(50, 40, 50);// Создание картыlet mapWorld = new Map();mapWorld.generation(scene);let controls = new Controls( new PointerLockControls(camera, document.body),  scene, mapWorld );renderer.domElement.addEventListener( "keydown", (e)=>{ controls.inputKeydown(e); } );renderer.domElement.addEventListener( "keyup", (e)=>{ controls.inputKeyup(e); } );document.body.addEventListener( "click", (e) => { controls.onClick(e); }, false );function update(){// передвижение/камераcontrols.update();};GameLoop();// Игровой циклfunction GameLoop() {update();render();requestAnimationFrame(GameLoop);}// Рендер сцены(1 кадра)function render(){renderer.render(scene, camera);}// обновление размера игрыwindow.addEventListener("resize", function() {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);});

Настройки


В настройки можно было вынести и другие параметры, например, настройки three.js, но я сделал без них и сейчас здесь лишь пара параметров отвечающие за размер блоков.

settings.js
export class Settings {constructor() {// площадь блокаthis.blockSquare = 5;// размер и площадь чанкаthis.chunkSize = 16;this.chunkSquare = this.chunkSize * this.chunkSize;}}

Генерация карты


В классе Map у нас есть несколько свойство которые отвечают за кеш материалов и параметры для шума Перлина. В методе generation мы загружаем текстуры, создаем геометрию и меш. noise.seed отвечает за стартовое зерно для генерации карты. Можно рандом заменить на статичное значение чтобы карты всегда была одинаковая. В цикле по X и Z координатам начинаем расставлять кубы. Y координата генерируется за счет библиотеки pretlin.js. В конечном итоге мы добавляем куб с нужными координатами на сцену через scene.add( cube );

generationMap.js
import * as THREE from './three.module.js';import { Settings } from "./settings.js";export class Map {    constructor(){this.materialArray;this.xoff = 0;this.zoff = 0;this.inc = 0.05;this.amplitude = 30 + (Math.random() * 70);    }    generation(scene) {const settings = new Settings();const loader = new THREE.TextureLoader();const materialArray = [new THREE.MeshBasicMaterial( { map: loader.load("../texture/dirt-side.jpg") } ),new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } ),new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-top.jpg') } ),new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-bottom.jpg') } ),new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } ),new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } )];this.materialArray = materialArray;const geometry = new THREE.BoxGeometry( settings.blockSquare, settings.blockSquare, settings.blockSquare);noise.seed(Math.random());for(let x = 0; x < settings.chunkSize; x++) {for(let z = 0; z < settings.chunkSize; z++) {let cube = new THREE.Mesh(geometry, materialArray);this.xoff = this.inc * x;this.zoff = this.inc * z;let y = Math.round(noise.perlin2(this.xoff, this.zoff) * this.amplitude / 5) * 5;cube.position.set(x * settings.blockSquare, y, z * settings.blockSquare);scene.add( cube );}}}}

Камера и управление


Я уже говорил, что controls принимает параметры в виде камеры, сцены и карты. Также в конструкторе мы добавляем массив keys для клавиш и movingSpeed для скорости. Для мыши у нас есть 3 метода. onClick определяет какая кнопка нажата, а onRightClick и onLeftClick уже отвечают за действия. Правый клик(удаление блока) происходит через raycast и поиска пересеченных элементов. Если их нет, то прекращаем работу, если есть, то удаляем первый элеент. Левый клик работает по схожей системе. Для начала создаем блок. Запускаем рейкаст и если есть блок который пересек луч, то получаем координаты этого блока. Далее определяем с какой стороны произошел клик. Меняем координаты для созданного куба в соответствии со стороной к которой мы добавляем блок. градация в 5 единиц т.к. это размер блока(да здесь можно было использовать свойство из settings).

Как работает управление камерой?! У нас есть три метода inputKeydown, inputKeyup и update. В inputKeydown мы добавляем кнопку в массив keys. inputKeyup отвечает за очистку кнопок из массива которые отжали. В update идет проверка keys и вызывается moveForward у камеры, параметры которые принимает метод это скорость.

controls.js
import * as THREE from "./three.module.js";import { Settings } from "./settings.js";export class Controls {constructor(controls, scene, mapWorld){this.controls = controls;this.keys = [];this.movingSpeed = 1.5;this.scene = scene;this.mapWorld = mapWorld;}// кликonClick(e) {e.stopPropagation();e.preventDefault();this.controls.lock();if (e.button == 0) {this.onLeftClick(e);} else if (e.button == 2) {this.onRightClick(e);}}onRightClick(e){// Удаление элемента по кликуconst raycaster = new THREE.Raycaster();raycaster.setFromCamera( new THREE.Vector2(), this.controls.getObject() );let intersects = raycaster.intersectObjects( this.scene.children );if (intersects.length < 1)return;this.scene.remove( intersects[0].object );}onLeftClick(e) {const raycaster = new THREE.Raycaster();const settings = new Settings();// Поставить элемент по кликуconst geometry = new THREE.BoxGeometry(settings.blockSquare, settings.blockSquare, settings.blockSquare);const cube = new THREE.Mesh(geometry, this.mapWorld.materialArray);raycaster.setFromCamera( new THREE.Vector2(), this.controls.getObject() );const intersects = raycaster.intersectObjects( this.scene.children );if (intersects.length < 1)return;const psn = intersects[0].object.position;switch(intersects[0].face.materialIndex) {case 0:cube.position.set(psn.x + 5, psn.y, psn.z); break;case 1: cube.position.set(psn.x - 5, psn.y, psn.z); break;case 2:cube.position.set(psn.x, psn.y + 5, psn.z); break;case 3:cube.position.set(psn.x, psn.y - 5, psn.z); break;case 4:cube.position.set(psn.x, psn.y, psn.z + 5); break;case 5: cube.position.set(psn.x, psn.y, psn.z - 5); break;}this.scene.add(cube);}// нажали на клавишуinputKeydown(e) {this.keys.push(e.key);}// отпустили клавишуinputKeyup(e) {let newArr = [];for(let i = 0; i < this.keys.length; i++){if(this.keys[i] != e.key){newArr.push(this.keys[i]);}}this.keys = newArr;}update() {// Движение камерыif ( this.keys.includes("w") || this.keys.includes("ц") ) {this.controls.moveForward(this.movingSpeed);}if ( this.keys.includes("a") || this.keys.includes("ф") ) {this.controls.moveRight(-1 * this.movingSpeed);}if ( this.keys.includes("s") || this.keys.includes("ы") ) {this.controls.moveForward(-1 * this.movingSpeed);}if ( this.keys.includes("d") || this.keys.includes("в") ) {this.controls.moveRight(this.movingSpeed);}}}

Ссылки


Как и обещал. Весь материал который пригодится.
Если есть желание, то на можете добавить свой функционал к проекту на гитхаб.

perlin.js
three.js
GitHub
Подробнее..
Категории: Javascript , Webgl , Gamedev , Three.js , Minecraft

Как собрать паука в Godot, Unigine или PlayCanvas

04.01.2021 22:21:38 | Автор: admin
С наступившим 21-м годом 21-го века.
В данной статье пробегусь по особенностям работы в трёх игровых движках, на примере написания кода для паукообразного средства передвижения.




Общая структура паукомобиля подразумевалась следующая есть 6 лап, на каждой копия управляющего скрипта, который отслеживает перемещение основной конструкции и переставляет лапу в новую позицию, когда она отдаляется на определённое расстояние.
Для более точной посадки лап планировалось добавить и рейкасты, но, допустим, в том же Godot я хотел сделать паука для игры с видом сверху, при котором подобная точность не особо нужна.
Таким образом задача состояла в формировании нужной структуры внутри движка и написании кода отдельной лапки. Вот что получилось в разных средах разработки:



Godot


Здесь у меня уже был готов небольшой проект с машинками и паука я решил добавить в одну из сцен (префабов), содержащую в себе подкласс машинок, которые не имеют колёс.
Сама сцена specific_base устроена таким образом, что в основе узел-пустышка, который просто висит где-то в мире, без движения, а по миру перемещается kinematic body внутри него. Камера находится внутри сцены, но вне body, просто следуя за ним.



Для создания паука я добавил отдельный узел внутрь body, содержащий в себе точки-пустышки для расположения лап (место их постановки на землю, а не места крепления к телу).



Сами лапы для удобства разместил здесь же, в этой сцене, но вне body. Управляющий каждой из них скрипт, помимо переноса лапы, будет постоянно разворачивать её к центру паука.


Код внутри редактора Godot

Пишем код. Я использую GDScript, потому как особого смысла писать именно на C# в Годо не вижу (не настолько фанат фигурных скобочек):

extends Spatialexport var distance = 2.5#максимальная дистанция, после которой случится перерасчётexport var step = 1#переменная для дополнительного смещения лап (вещь необязательная)#ссылки на центр паука и одну из позиций лап, а также элементы для их храненияexport (NodePath) var spidercenter = nullvar trg_centerexport (NodePath) var spiderleg = nullvar trg_leg#переменные для расстояний по осям x и zvar x_dis = 0.0var z_dis = 0.0#переменная-таймер, а также флагvar time_lag = -1.0# инициализацияfunc _ready():self.hide()#скрыть лапуtrg_center = get_node(spidercenter)#запомнить объектыtrg_leg = get_node(spiderleg)LegPlace()#один раз вызвать установку лапки на позицию# основной циклfunc _process(delta):        #развернуть лапу в направлении центра паука. можно ввести таймер, чтобы делать это через малые интервалыself.look_at(trg_center.global_transform.origin, Vector3(0,1,0))        #включить видимость, если лапа была невидимой. это делалось для того, чтобы показывать её снова, после того как она скрывается в момент перестановки (чтобы перестановка выглядела как появление лапы в новой позиции уже развёрнутой в нужную сторону, а не перенесением с последующим разворачиванием). на самом деле можно было вынести внеочередной разворот и последующий показ лапы в LegPlaceif self.visible == false: self.show()if time_lag>=0:#если флаг-таймер запущен, то наращивать его значениеtime_lag +=1*delta if time_lag>0.06:#при истечении задержки сбросить флаг и вызвать перерисовкуtime_lag = -1.0LegPlace()else:#пока флаг неактивен считать дистанции от лапы до позиции лапы по двум осямx_dis = abs(trg_leg.global_transform.origin.x - self.global_transform.origin.x)z_dis = abs(trg_leg.global_transform.origin.z - self.global_transform.origin.z)if (x_dis + z_dis) > distance:#если дистанция больше лимита, запустить флагtime_lag = 0.0passfunc LegPlace():#собственно, сама функция перестановки лапыself.hide()step = step*(-1)self.global_transform.origin = trg_leg.global_transform.origin+Vector3(0,0,0.5*step)



Видеонарезка с тестом паука в Godot. Бонусом сюда же вошли моменты из отдельной игры с терраформированием воксельного мира, где тоже используется паучок, но чисто анимированный, а также демка с машинками на playcanvas, до того как туда была добавлена форма паука.



Unigine


После того, как была готова реализация для Godot, я решил перенести это решение и в Unigine engine. Там у меня тоже имелся проект с машинками, правда чтобы не перегружать его я сделал отдельный паучий форк, чтобы впоследствии, наверное, вовсе убрать из него колёса и развивать как-то отдельно.



Тут имеется вся сцена игрового мира, в котором располагается корпус машинки игрока. В начале игры к этому корпусу пристыковываются отдельно расположенные колёса.



Завожу пустышку внутри корпуса, внутри которой будут находится точки, задающие позиции лап. Лапы размещаются просто в игровом мире. Само движение оставил реализованным через колёса, но их визуальное представление отключено.


Unigine для редактирования кода запускает внешнюю среду

Код:

using System;//стандартная "шапка"using System.Collections;using System.Collections.Generic;using Unigine;//уникальный идентификатор компонента, генерируемый при создании скрипта[Component(PropertyGuid = "5a8dd6f85781adf7567432eae578c5414581ddac")]public class theLegBehavior : Component{[ShowInEditor][Parameter(Tooltip = "CenterSpider")]//указатель на центр паукаprivate Node spiderCenter = null;[ShowInEditor][Parameter(Tooltip = "Target Leg Point")]//указатель на точку крепленияprivate Node legPoint = null;//переменные для вычислений дистанций по осямprivate float x_dis= 0.0f;private float z_dis= 0.0f;private float ifps;//переменная для дельтатаймprivate float time_lag = -1.0f;//таймер-флагprivate void Init()//инициализация{node.Enabled = false;//скрыть лапуLegPlace();//вызвать перестановку лапы}private void Update()//основной цикл{ifps = Game.IFps;//сохранить дельтатаймif (time_lag>=0.0f){//далее уже знакомая конструкцияtime_lag += 1.0f*ifps;if (time_lag>=0.6f) {time_lag = -1.0f;LegPlace();}}else{x_dis = MathLib.Abs(legPoint.WorldPosition.x - node.WorldPosition.x);z_dis = MathLib.Abs(legPoint.WorldPosition.z - node.WorldPosition.z);            if (x_dis + z_dis > 0.8f){time_lag = 0.0f;}}}        //функция перерасчёта положения лапы. здесь уже финальный показ лапы встроен внутрь функции. также тут происходит единичный разворот в сторону центра паука. а постоянный разворот считается вне этого скрипта, а в отдельном, наброшенном на лапу скрипте, хотя я по сути уже это вынес оттуда и можно включить в Update этого скрипта.private void LegPlace(){node.Enabled = false;vec3 targetDirection = vec3.ZERO;targetDirection = (legPoint.WorldPosition - node.WorldPosition);quat targetRot = new quat(MathLib.LookAt(vec3.ZERO, targetDirection, vec3.UP, MathLib.AXIS.Y));quat delta = MathLib.Inverse(targetRot);delta.z = 0;delta.Normalize();node.WorldPosition = legPoint.WorldPosition;        targetDirection = (spiderCenter.WorldPosition - node.WorldPosition);node.SetWorldDirection(targetDirection, vec3.UP, MathLib.AXIS.Y);node.Enabled = true;}}



Видеонарезка паучьего теста в Unigine



PlayCanvas


PlayCanvas игровой движок под webGL, использующий javascript. Недавно начал в нём разбираться. Напоминает нечто среднее между Unity и Godot, но с разработкой онлайн редактор открывается в браузере.

В данном случае я переделал один из предлагаемый данной платформой примеров, добавив туда пару своих моделек машин, немного дополнительного функционала вроде прыжков, дописав камеру и поигравшись с материалами/настройками.

Визуальное представление базовой машины в исходном примере было задано цельной моделью, которую я частично отключил, оставив пока только колёса, которые изнутри модели достаёт её управляющий скрипт (то есть они не заведены отдельными узлами в иерархии сцены). Это визуальное представление во время игрового процесса цепляется к движущейся физической колёсной модели.
Для реализации паука внутри данного проекта я завёл паучий центр внутри визуального представления машинки. Тело паука для удобства вложено внутрь него же, так как хотелось сразу сделать переключение на форму паука и обратно.



Таким образом, местный паук тоже сделан поверх езды на колёсах, а сами колёса скрыты. Возможно, правильнее было бы крепить паучий каркас к узлу физики, но переключение между формами уже расположено в скрипте узла с визуалом машинки, и прочие модели находятся внутри этого узла, поэтому для простоты выбрал такой вариант. На деле же получается, что визуал по своему скрипту перемещается за физикой, а скрипты лап уже смотрят на паучий каркас, перемещающийся вместе с визуалом. По идее тут может возникать некоторая рассинхронизация, по крайней мере я заметил, что на слабых устройствах некоторые лапки не успевают вычислять позиции и перемещаются лишь некоторые.



Лапы расположены в самой сцене, но для удобства их включения/отключения собраны внутри отдельного узла, чтобы можно было просто отключать его, а не каждую по отдельной ссылке.


В playcanvas редактор кода запускается вновой вкладке браузера

Код:
var TheLegBehavior = pc.createScript('theLegBehavior');//ссылка на центр паукаTheLegBehavior.attributes.add('N_spiderCenter', { type: 'entity' });//ссылка на точку постановки этой лапкиTheLegBehavior.attributes.add('N_legPoint', { type: 'entity' });this.x_dis = 0.0;this.z_dis = 0.0;this.time_lag = -1.0;// initialize code called once per entityTheLegBehavior.prototype.initialize = function() {        };// update code called every frameTheLegBehavior.prototype.update = function(dt) {    if (this.N_spiderCenter) {        this.entity.lookAt(this.N_spiderCenter.getPosition());    }};// постапдейтTheLegBehavior.prototype.postUpdate = function(dt) {    //    if (this.N_spiderCenter) {//        this.entity.lookAt(this.N_spiderCenter.getPosition());//    }    if (time_lag>=0.0){        time_lag+=1.0*dt;        if (time_lag>=0.06){            time_lag=-1.0;            this.LegUpdate();        }            } else {                x_dis = Math.abs(this.entity.getPosition().x-this.N_legPoint.getPosition().x);        z_dis = Math.abs(this.entity.getPosition().z-this.N_legPoint.getPosition().z);                if ((x_dis+z_dis)>3.0){         time_lag=0.0;        }                    }};TheLegBehavior.prototype.LegUpdate = function() {        if (this.N_legPoint) {        this.entity.setPosition(this.N_legPoint.getPosition());    }    //    if (this.N_spiderCenter.enabled === false) {//        this.entity.enabled = false;//    }//    if (this.N_spiderCenter.enabled === true) {//        this.entity.enabled = true;//    }    };


В целом, пока получилась заготовка паучка с четырьмя лапками и не совсем оптимальными расчётами.

Потестить получившегося на текущий момент кадавра можно здесь:
https://playcanv.as/p/rOebDLem/

Я лично пробовал запускать на не слишком мощном смартфоне через Chrome и Dolphin, графика становится похожей на PsOne и вычисления для некоторых лап не работают, пока на экране виден уровень, у края карты лапы проявляются. На слабом ноутбуке наблюдались очень сильные тормоза в Chrome, но в Firefox всё катается нормально. На ноутбуке с дискретной видеокартой и на стационарнике летает в обоих этих браузерах.

На ПК, в отличие от смартфонов в этой демке работает прыжок (по кнопке пробел), заготовка стрейфа (Q и E) и перезагрузка уровня (на R).

Итог



Как видите, языки и движки разные, но тем не менее многие вещи в семействе скриптовых языков делаются довольно похожим образом. А как часто вы осваивали новые для себя игровые движки и фреймворки, с какими основными трудностями столкнулись?
Подробнее..

Рендеринг шрифтов для WebGL при помощи инстумента msdf-bmfont-xml и технологии MSDF

21.03.2021 14:19:44 | Автор: admin

Пример: https://openglobus.org/examples/fonts/fonts.html

18/3/2021 Наконец-то была закончена интеграция инструмента msdf-bmfont-xml для библиотеки openglobus. Текстовые метки стали выглядеть гораздо красивее! Мне помог инструмент msdf-bmfont-xml для создания атласов шрифтов и рендеринга текстур для (multichannel signed distance fields) MSDF.

Пример по ссылке: https://openglobus.org/examples/fonts/fonts.htmlПример по ссылке: https://openglobus.org/examples/fonts/fonts.html

msdf-bmfont-xml предлагает широкие возможности для формирования текстурных атласов из шрифтов в формате ttf. В нашем случае, текстура атласа представляет из себя многоканальную карту расстояний (multichannel signed distance fields), которая позволяет отображать острые углы букв, в отличии от оригинального signed distance field.

В этой статье я хочу расскать, что из себя представляет атлас для хранения букв, и как отображать текст при помощи WebGL.

Текстурный атлас msdf-bmfont-xml

Текстурный атлас шрифта, это текстура с сохраненными на ней картинками символов. Каждая картинка имеет соответствующие текстурные координаты.

Текстура 512x512 для атласа шрифта Roboto-RegularТекстура 512x512 для атласа шрифта Roboto-Regular

Для создания атласа я использую комманду:

msdf-bmfont.cmd - reuse -i .\charset.txt -m 512,512 -f json -o %1.png -s 32 -r 8 -p 1 -t msdf %1

где %1имя файла шрифта в формате ttf, charset.txtэто файл с набором символов для которых строится атлас, про остальные параметры можно узнать на официальной страничке репозитория msdf-bmfont-xml:

При успешном выполненении команды создаются несколько файлов, нас будут интересовать текстура атласа в формате png и описание атласа в формате json.

В полученном файле описания json информация по символам хранится в разделе chars. Например, символ q на картинке атласа расположен в координатах x = 131, y = 356, width = 22, height = 32. т.е. координаты левого правого угола [131, 356] и правого нижнего соответственно [131+22, 356+32]. Таким образом, если размер текстуры атласа равен 512 на 512 пикселей, значит текстурные координат сомвола q соответственно будут равны [131/512, 356/512 ] и [153/512, 388/512]. Если передать эти текстурные координаты в шейдер, который рисует прямоугольник, то мы увидем в этом прямоугольнике наш символ. Кроме того, у нас имеется ширина и высота символа, согласно этим данным мы устанавливаем размер прямоугольника, чтобы символ выглядел пропорционально правильным.

{    id: 113,    char: "q",    width: 22,    height: 32,    xoffset: -3,    yoffset: 11,    xadvance: 18,    x: 131,    y: 356,    ...}

Другими важными параметрами для позиционирования символа являются:

xoffsеtСмещение символа по горизонтали

yoffsetСмещение символа по вертикали

xadvanceШирина символа; расстояние от левой границы символа до начала следующего символа в строке.

А также id символа по которому можно идентифицировать символ в таблице горизонтальных кернингов. Кернингэто расстояние между двумя специфическими символами.

Пример: изображения строки Wg! шрифт Arial

Парметры символов:

W: width: 37, height: 31, xoffset: -4, yoffset: 4, xadvance: 30g: width: 23, height: 32, xoffset: -3, yoffset: 10, xadvance: 18! : width: 11, height: 31, xoffset: -1, yoffset: 4, xadvance: 9

Центром координат является левый верний угол, изначально символы располагаются на одной прямой относительно верхней зеленой линии по вертикали и относительно левой зеленой линии по горизонтали, и имеют смещение относительно друг друга согласно параметрам width.

Следующая картинка показывает смещение символов относительно горизонтальной (центральной) оси по вертикали, параметр yoffsset.

Голубыми линиями обозначен параметр xadvance (расстояние до следующего символа), также каждый символ смещен по горизонтали согласно параметру xoffset.

Текущий вариант можно считать готовым, однако для лучшего качества следует применить параметры для кернинга, т.е. смещение относительно друг друга двух специфических символов. В этом примере я не показываю кернинги; кернинг учитывается в параметре xoffset во время отображения текста.

Рендеринг текста MSDF

Отрисовка массива вершин символа производится методом gl.drawArrays, где исходным буффером является буффер массива вершин для атрибута a_vertices:

vec2 a_vertices = [0, 0, 0, -1, 1, -1, 1, -1, 1, 0, 0, 0]

Основные параметры символов предварительно нормализуются при построении атласа шрифта:

imageSize = 512; //размер текстуры атласаnWidth = width / imageSize; //нормализованная ширина символаnHeight = height / imageSize; //нормализованная высота глифаnAdvance = xadvance / imageSize; //нормализованный размер глифа до следующего символа в строкеnXOffset = xoffset / imageSize; //нормализованное смещение по горизонталиnYOffset = 1.0 - yoffset / imageSize; //нормализованное смещение по вертикали

Шейдер GLSL

Исходник: https://github.com/openglobus/openglobus/blob/master/src/og/shaders/label.js

// Vertex shader:// ...vec2 v = screenPos + (a_vertices * a_gliphParam.xy + a_gliphParam.zw + vec2(advanceOffset, 0.0)) * a_size;// Где:// screenPos - экранные координаты строки// a_vertices - Координаты вершин// a_gliphParam - вектор с метриками символа, где:// x - nWidth, y - nHeight, z - nXOffset, w - nYOffset// advanceOffset - сумарное смещение по параметру nAdvance, каждого последующего сомвола в строке// a_size - экранные размеры строки в пикселях // Fragment shader:// ...const float imageSize = 512.0;const float distanceRange = 8.0;layout(location = 0) out vec4 outScreen;float median(float r, float g, float b) {    return max(min(r, g), min(max(r, g), b));}float getDistance() {    vec3 msdf = texture(fontTexture, v_uv).rgb;    return median(msdf.r, msdf.g, msdf.b);}void main () {    vec2 dxdy = fwidth(v_uv) * vec2(imageSize);    float dist = getDistance() + min(v_weight, 0.5  1.0 / DIST_RANGE) - 0.5;    float opacity = clamp(dist * distanceRange / length(dxdy) + 0.5, 0.0, 1.0);    outScreen = vec4(v_rgba.rgb, opacity * v_rgba.a);}// Где:// fontTexture - текстура атласа шрифтов// v_weight - ширина символа от 0 до 1, используется для окантовки, по умолчанию равен 0.// Окантовка рисуется ПЕРВМ проходом с заданным v_weight.// v_uv - текстурные координаты символа в атласе шрифтов// v_rgba - цвет символа, или окантовки// ...

Надеюсь, что я достаточно понятно объяснил, как работать с атласами шрифтов и как я использую msdf-bmfont-xml для своего проекта. Этот подход существенно улучшил качество текстовых меток на карте.

Пример редактора планировщика маршрута БПЛА компании Microavia c использованием библиотеки OpenglobusПример редактора планировщика маршрута БПЛА компании Microavia c использованием библиотеки Openglobus

Пишите в комментариях, чем вы пользуетесь, для рендеринга шрифтов, и как на ваш взгляд можно улучшить качество текстовых меток?

Если у вас возникнут вопросы по применению моей рекомендации можете задать их на openglobus форуме https://groups.google.com/forum/#!forum/openglobus, и я обязательно отвечу!

Желаю Вам хорошего настроения!

Полезные источники

Подробнее..

Как я написал браузерный 3D FPS шутер на Three.js, Vue и Blender

07.05.2021 02:13:27 | Автор: admin
Стартовый экран игрыСтартовый экран игры

Мотивация

На пути каждого коммерческого разработчика (не только кодеров, но, знаю, у дизайнеров, например, также) рано или поздно встречаются топкие-болотистые участки, унылые мрачные места, блуждая по которым можно вообще забрести в мертвую пустыню профессионального выгорания и/или даже к психотерапевту на прием за таблетками. Работодатели-бизнес очевидно задействует ваши наиболее развитые скилы, выжимая по максимуму, стек большинства вакансий оккупирован одними и теми же энтерпрайз-инструментами, кажется, не для всех случаев самыми удачными, удобными и интересными, и вы понимаете что вам придется именно усугублять разгребать тонну такого легаси Часто отношения в команде складываются для вас не лучшим образом, и вы не получаете настоящего понимания и отдачи, драйва от коллег Умение тащить себя по-мюнхаузеновски за волосы, снова влюбляться в технологии, увлекаться чем-то новым [вообще и/или для себя, может быть смежной областью], имхо, не просто является важным качеством профессионала, но, на самом деле, помогает разработчику выжить в капитализме, оставаясь не только внешне востребованным, конкурентоспособным с наступающей на пятки молодежи, но, прежде всего, давая энергию и движение изнутри. Иногда приходится слышать что-нибудь вроде: а вот мой бывший говорил, что если бы можно было не кодить, он бы не кодил!. Да и нынешняя молодежь осознала что в сегодняшней ситуации честно и нормально зарабатывать можно только в айти, и уже стоят толпою на пороге HR-отдела... Не знаю, мне нравилось кодить с детства, а кодить хочется что-нибудь если не полезное, то хотя бы интересное. Короче, я далеко не геймер, но в моей жизни было несколько коротких периодов когда я позорно загамывал. Да само увлечение компьютерами в детстве началось, конечно же, с игр. Я помню как в девяностые в город завезли Спектрумы. Есть тогда было часто практически нечего, но отец все-таки взял последние деньги из заначки, пошел, отстоял невиданно огромную очередь и приобрел нам с братом нашу первую чудо-машину. Мы подключали его через шнур с разъемами СГ-5 к черно-белому телевизору Рекорд, картинка тряслась и моргала, игры нужно было терпеливо загружать в оперативную память со старенького кассетного магнитофона [до сих пор слышу ядовитые звуки загрузки], часто переживая неудачи... Несмотря на то что ранние программисты и дизайнеры умудрялись помещать с помощью своего кода в 48 килобайт оперативной памяти целые миры с потрясающим геймплеем, мне быстро надоело играть и я увлекся программированием на Бейсике)), рисовал спрайтовую графику (и векторная трехмерная тогда тоже уже была, мы даже купили сложную книжку), писал простую музыку в редакторе... Так вот, некоторое время назад мне опять все надоело, была пандемийная зима и на велике не покататься, рок-группа не репетировала Я почитал форумы и установил себе несколько более-менее свежих популярных игр, сделанных на Unity или Unreal Engine, очевидно. Мне нравятся РПГ-открытые миры-выживалки, вот это все... После работы я стал каждый вечер погружаться в виртуальные миры и рубиться-качаться, но хватило меня ненадолго. Игры все похожи по механикам, однообразный геймплей размазан по небольшому сюжету на кучу похожих заданий с бесконечными боями Но, самое смешное, это реально безбожно лагает в важных механиках. Лагают коммерческие продукты которые продают за деньги А любой баг, имхо, это сильное разочарование он мгновенно выносит из виртуальной среды, цифровой сказки в реальный мир Конечно, отличная графика, очень круто нарисовано. Но, утрируя, я понял что все эти поделки на энтерпрайзных движках, по сути даже не кодят. Их собирают менеджеры и дизайнеры, просто играясь с цветом кубиков, но сами кубики, при этом практически не меняются... Вообщем, когда стало совсем скучно, я подумал что а я ведь тоже так могу, да прямо в браузере на богомерзком непредназначенным для экономии памяти серьезного программирования джаваскрипте. Решил наконец полностью соответствовать тому что все время с умным видом повторяю сыну: уметь делать игры, намного интереснее чем в них играть. Одним словом, я задался целью написать свой кастомный браузерный FPS-шутер на открытых технологиях.

Итак, на данный момент, первый результат по этой долгоиграющей таски на самого себя можно тестить: http://robot-game.ru/

Стек и архитектура

Вполне может быть, что я не вкурсе чего-то (ммм на ум приходит что-нибудь вроде quakejs и WebAssembly), но, с основной технологией было, походу, особо без вариантов. Библиотека Three.js давно привлекала мое внимание. Кроме того, в реальной коммерческой практике, несколько раз, но уже приходилось сталкиваться с заказами на разработку с ее использованием. На ней я сделал собственно саму игру.

Очевидно, что нужно что-то вокруг для простого интерфейса пользователя: шкал, текстовых сообщений, инструкций, контролов настроек, вот этого всего. Я решил поленился, не усложнять себе жизнь и использовать любимый фреймворк Vue 2, хотя, надо было, конечно, писать на свежем, похожем по дизайну и еще более прогрессивном по сути молниеносном Svelte. Но так как хорошенько разобраться предстояло, прежде всего, с Three, думаю, это было правильное решение. Хорошо знакомый и предсказуемый, лаконичный, изящный, удобный и эффективный Vue, позволил практически не тратить время на внешний пользовательский интерфейс.

Когда-то давно я работал дизайнером на винде и достаточно бойко рисовал 2D в Иллюстраторе, но навыков 3D у меня никаких не было. А вот в процессе создания шутера пришлось пойти, скачать и установить одним кликом на свой нынешний Linux Blender. Я быстро научился рисовать с помощью примитивов мир, отдельные объекты, и даже научился делать UV-развертки на них. Но! В целях простоты, скорости работы и оптимизации объема ассетов в моей нынешней реализации не используются текстурные развертки. Я просто подгружаю чистые легковесные бинарные glTF: .glb-файлы и натягиваю на них всего несколько вариантов нескольких текстур уже в джаваскрипте. Это приводит к тому что текстуры на объектах искажаются в разных плоскостях, но на основном бетоне для стен, смотрится даже прикольно, такой разный, рваный ритм. Кроме того, сейчас персонажи не анимируются пока не было времени изучить скелетную анимацию. Одной из основных целей написания этой статьи является желание найти (по знакомым не получилось) специалиста который поможет довести проект до красоты (очень хочется) и согласится добавить совсем немного анимаций на мои .glb (об условиях договоримся). Тогда враги, будут погружаться в виде glTF со встраиванием: .gltf-файлов со встроенными текстурами и анимациями. Сейчас уже есть два вида врагов: ползающие-прыгающие наземные дроны-пауки и их летающая версия. Первых нужно научить шевелить лапками при движении и подбирать их в прыжке, а вторым добавить вращение лопастей.

Модель дрона-паука в BlenderМодель дрона-паука в Blender

Для того чтобы игру нельзя было тупо-легко прочитить через браузерное хранилище я добавил простенький бэкенд на Express с облачной MongoDB. Он хранит в базе данные о прогрессе пользователя по токену, который на фронте записывается в хранилище. Хотелось сделать не просто FPS-шутер, а привнести в геймплей элементы РПГ. Например, в нынешней реализации мир делиться на пять больших уровней-локаций между которыми можно перемещаться через перезагрузку. При желании локации можно быстро дорисовывать из уже имеющихся и добавлять в игру, указывая только двери входа и выхода, стартовую и конечную координату, хорошее направление камеры для них (при переходе живого персонажа через дверь текущее направление сохраняется-переносится). На каждом уровне есть только одна формальная цель найти и подобрать пропуск к двери на следующий уровень. Пропуски не теряются при проигрыше на локации (только при выборе перехода на стартовый уровень после выигрыша на последнем пятом). А вот враги и полезные предметы цветы и бутылки при переходе между локациями, проигрыше или перезагрузке страницы пока выставляются заново согласно основной glb-модели одновременно и схеме, и визуальной клетке локации об этом дальше. И тут вот первое важное про архитектуру: мой фронтенд это совсем примитивное SPA. Vue, например, ни для чего не нужен роутер. Вероятно, я получу негативную реакцию некоторых продвинутых читателей, после того, как сообщу что потратил кучу времени для того чтобы попробовать организовать перезагрузку-очистку сцены внутри системы и пока с самым провальным результатом. Вот к такой спорной мысли я пришел в процессе своих экспериментов: самый эффективный, простой, даже, в этой ситуации, правильный и при этом, конечно же, топорный подход, это нативный форс-релоад после того как мы сохраняем или обнуляем данные пользователя на бэкенде:

window.location.reload(true);

А потом просто дадада считываем их обратно )) и строим всю сцену заново, с чистого листа, так сказать. Тут, конечно, можно было бы улучшить прокидывать пользователя через хранилище вместо того чтобы ожидать разрешения запроса, но это не критично, в данном случае. Небольшое количество оптимизированных текстур (меньше полтора мегабайта сейчас), сильно компрессированного аудио (MP3, понятно: 44100Гц 16 бит, но с сильным сжатием 128 кбит/с меньше полтора мегабайта все вместе сейчас), основная модель-локация весящая около 100Кб и модели отдельных объектов каждая еще меньше... Я добился того что переход между локациями полная перезагрузка мира занимает вполне приемлемое время, судя по записи перфомансов примерно две с чем-то, три секунды. И это, кажется, меньше чем во всех шовных открытых мирах от энтерпрайза которые я видел. Продвинуто бесшовный я тоже один нашел и поиграл, но он лагал хуже всех, и когда сюжет наконец двинулся с мертвой точки вдруг перестали работать сейвы; тут я уже забил

Все использующиеся в игре текстурыВсе использующиеся в игре текстурыПерфомансПерфоманс

Хочется сразу сказать что техлиды и сеньоры с менторским тоном и заоблачной экспертизой в микробенчмаркинге в комментариях только приветствуются. Это же вообще самое забавное и интересное на Хабре когда лиды с сеньорами начинают рубиться в комментариях за стоимость операций в джаваскрипте и то, чей микробенчмаркинг заоблачнее! Остается только надеятся на то, что когда вы будете размазывать мой форсрелоад как дешёвое и сердитое средство изменения сцены вы обязательно продемонстрируете ваши работающие примеры в которых сцена Three с большим количеством разнообразных объектов на ней очищается и заново инициализируется через свои внутренние методы (например, без перезагрузки текстур и прочих ассетов, аудио). Я же не говорю что это невозможно, это очевидно дорого. Намного дороже чем просто сделать форсрелоад. Понятно что хороший проект это прежде всего кодовая база которая может и должна легко развиваться. Но невозможно прикрутить все фичи сразу, а использование дешевого релоада сейчас никак не блокирует добавление более сложного функционала в будущем. Да и кроме дешевизны более простой подход и идеологически привлекателен. Я убежден что хороший код это простой и понятный код, хороший подход простой подход, точно так же как и интерфейс который они предоставляют. Простое решение лучше сложного, особенно если мы только начинаем строить что-то.

Для того чтобы избежать лишних сложностей в моей реализации сцена практически неизменна. Она разворачивается, запускается и дальше функционирует в некотором постоянном виде [порождая и уничтожая только выстрелы и взрывы] пока не происходит переход в другую локацию (или проигрыш на этой). Конкретнее: cейчас я нигде кроме удаления не подлежащих внешнему учету выстрелов и взрывов не использую scene.remove(object.mesh) например при сборе героем полезных предметов, делая вместо этого:

// встроенное свойство на Object3D в Threeobject.mesh.visible = false;// кастомный флаг кастомного массива объектовobject.isPicked = true;

Поэтому мы, например, можем даже использовать свойство id: number mesh`ей вместо uuid: string для учета и идентификации объектов. Так как все подлежащие учету объекты всегда остаются на сцене мы можем быть уверены что Three не поменяет айдишники, сдвинув нумерацию под коробкой при удалении элемента (но если вы хотите все-таки удалять что-то такое просто опирайтесь на uuid при работе с этим).

Я нигде и не на чем не использую .dispose(), так как мне просто нечего удалять. В документации библиотеки сказано что лучший момент и повод для этого переход между уровнями, когда можно и нужно, например удалить ненужные текстуры. Как вы видите выше, текстур у нас совсем немного и они все всегда нужны.

Посмотрим на структуру проекта:

. /public // статические ресурсы   /audio // аудио     ...   /images // изображения     /favicons // дополнительные фавиконки для браузеров       ...     /modals // картинки для информационных панелей       /level1 // для уровня 1         ...       ...     /models       /Levels         /level0 // модель-схема Песочницы (скрытый уровень 0 - тестовая арена)           Scene.glb         ...       /Objects          Element.glb          ...     /textures        texture1.jpg        ...   favicon.ico // основная фавиконка 16 на 16   index.html // статичный индекс   manifest.json // файл манифеста   start.jpg // картинка для репозитория ) /src   /assets // ассеты сорцов     optical.png // у меня один такой )))   /components // компоненты, миксины и модули     /Layout // компоненты и миксины UI-обертки над игрой       Component1.vue // копонент 1       mixin1.js // миксин 1       ...     /Three // сама игра        /Modules // готовые полезные модули из библиотеки          ...        /Scene           /Enemies // модули врагов             Enemy1.js             ...           /Weapon // модули оружия             Explosions.js // взрывы             HeroWeapon.js // оружие персонажа             Shots.js // выстрелы врагов           /World // модули различных элементов мира             Element1.js             ...           Atmosphere.js // модуль с общими для всех уровней объектами (общий свет, небо, звук ветра) и проверками взаимодействия между другими модулями           AudioBus.js // аудио-шина           Enemies.js // модуль всех врагов           EventsBus.js // шина событий           Hero.js // модуль персонажа           Scene.vue // основной компонент игры           World.js // мир   /store // хранилище Vuex     ...   /styles // стилевая база препроцессора SCSS     ...   /utils // набор утилитарных js-модулей для различных функциональностей     api.js // интерфейс для связи с бэкендом     constants.js // вся конфигурация игры и тексты-переводы     i18n.js // конфигурация переводчика     screen-helper.js // модуль "экранный помощник"     storage.js // модуль для взаимодействия с браузерным хранилищем     utilities.js // набор полезных функций-атомов   App.vue // "главный" компонент   main.js // эндпоинт сорцов Vue ... // все остальное на верхнем уровне проекта, как обычно: конфиги, gitignore, README.md и прочее

То как я работаю со стилями и оформлением компонент в UI-обертке подробно описано в моем первом масштабном тексте для сообщества. Целью данной статьи не является рассмотрение отдельных разрозненных особенностей перечисленных выше технологий и инструментов. Хочется рассказать об опыте, проблемах и найденных возможных путях их решениях которые возникли в результате практической попытки совместить данные технологии именно для решения конкретной задачи создания кастомного браузерного шутера.

Сейчас игра в спокойном состоянии когда потревоженных врагов нет или совсем мало, на компьютере с поддержкой GPU выдает практически коммерческие 60FPS в Google Chrome (ну или Yandex Bro). В Firefox игра запускается, но показатель производительности не менее чем в 2-3 раза ниже. А когда начинается мясо, появляется много потревоженных врагов, выстрелов и взрывов в Лисе процесс начинает лагать и может вообще повиснуть. Моя экспертиза в микробенчмаркинге сейчас пока не позволяет с умным видом рассуждать о причинах этой разницы. Будем считать что дело в более слабой поддержке WebGL и вычислительных способностях, что-то такое))...

Легенда

Так как выбранный мною тип игры совсем обычный классический FPS, пиф-паф, ойойой, мне была нужна актуальная захватывающая легенда. Прообраз главного героя Робот-собутыльник появился из нашего с друзьями музыкального рок-творчества: существует пластинка его имени и забавный клип про него... В игре речь, видимо, идет о потомках пьющего робота

Земля, далекое будущее. Люди давным-давно перебили друг-друга в ядерных войнах, выясняя кто правый, кто левый, кто белый, а кто красный и прочее. На не затронутых бомбардировками атоллах в Тихом Океане размножилось несколько рас человекоподобных роботов. Например, более человекоподобные, имитирующие органику, двуполость и личные отношения Собутыльники, которые перерабатывают животных и растительность в жизненную силу и спецэффекты. Внутри них, по тонким крепким трубкам, течет специальный сброженный органический микс, схожий с человеческим вином, приводя их в движение. Или более машиноподбные однополые Кибер-Танцоры, проповедующие медитативный Дзинь-Нойз. На почве гендерных и религиозных разногласий между культурами, конечно же, понеслась жестокая война.

Пара молодых Собутыльников отправились в свадебное путешествие по океану. Но катер внезапно дал течь и они с трудом дотянули до ближайшего атолла. Прекрасный на первый взгляд остров оказался опасной ловушкой, так как был давно облюбован для медитаций Танцорами.

Робот-Собутыльник приходит в себя на полу пыточной камеры тюрьмы Однополых... Баки пусты... Его мучители, видимо, решили что он уже не жилец, и оставили подыхать... На стене висит портрет легендарного Последнего Президента идеологического предтечи и кумира Танцоров человека, когда-то развязавшего последнюю в истории человечества войну

Аллегории прозрачные, конечно. Но сама рамочная идея про конфликт однополых и двуполых машин в будущем, имхо, замечательная. Так и будет, точно, все предпосылки уже налицо. ))

В игре пока нет самих главных врагов, только их дроны (нужен специалист по скелетной анимации отзовись!), но уже присутствуют специфические объекты и отдельная механика позволяющая глубже раскрыть генезис Собутыльников, историю их борьбы против демократической диктатуры Танцоров. Это информационные панели которые включаются когда герой находится поблизости и важно в одном помещении с ними. Ниже об этом будет подробнее, когда будем разбирать постройку мира и взаимодействие модулей.

ДашбордДашборд

Если подойти к панели и нажать E открывается модаль с исторической справкой:

Рассказ о будущем внутриРассказ о будущем внутри

Это задел для дальнейшего развития в сторону РПГ. Например на таких дашбордах несложно организовать торговый ларек, в котором можно было бы менять поверженный и подобранный металлолом на полезные цветы и бутылки.

Геймплей

В моей игре любой выстрел сталкиваясь с препятствием вызывает взрыв который уронит и врагов и героя на величину зависящую от расстояния. Поражение врага вызывает взрыв повышенной мощности следует быть осторожным в ближнем бою.

Фишку боям с двумя видами дронов с помощью единственного пока оружия виномета с оптическим прицелом сейчас добавляют похожие на чупики и хорошо заметные издалека, подозреваю психотропные цветы, употребление которых восполняет шкалу здоровья и прокачивает способности персонажа, ну или замедляет мир в случае зеленого цветка топлива для машины времени.

Цветы и бутылкиЦветы и бутылки

Так как у нас почти РПГ и возможно собирать полезный стафф перемещаясь назад по локациям цветы и бутылки имеют определенный вес и герой не может носит с собой больше 25 его единиц. Вес вместе с количеством возвращаемого приемом цветка здоровья помогает сбалансировать всю эту механику: тупые цветы дающий неуязвимость красный и прокачивающий виномет фиолетовый весят больше, а здоровье дают меньше чем более интересные желтый и зеленый.

Сейчас уже есть три уровня сложности игры они влияют на срок действия цветов, дистанцию на которой героя обнаруживают враги (если персонаж ползет может подобраться в два раза ближе) и, конечно же силу урона от выстрелова и взрывов.

Уровни сложностиУровни сложности

Если я найду скелетного аниматора и смогу довести до ума то что уже есть и потом продолжить развивать проект дальше, можно будет подумать о следующих фичах:

  • Разное оружие. Виномет потеряет прицел, но станет работать как пулемет-автомат не нужно будет все время нажимать на левую кнопку мыши. С прицелом будет снайперская винтовка стреляющая стальными снарядами (которые, например, можно делать из собранных разрушенных врагов тут бы пригодился уже упомянутый торговый ларек на дашбордах).

  • Еще один вид бутылок с крепышом часовые мины: установил быстро отбегаешь. Будут полезны для разрушения Танков с огромным здоровьем или крупных скоплений любых врагов.

  • Новые типы врагов. Танки медленные, но очень живучие и с убойным выстрелом. Стационарные дроны-пушки умеющие стрелять не в горизонтальной плоскости навесом как делают дроны сейчас, а под разными углами и двойными зарядами. Рядовые бойцы Танцоры Роботы-Курицы мой барабанщик почему-то их именно так видит. В идеале они высаживаются как спезназ, приземляясь на челноке в центр третьей локации когда герой на нее заходит. В пятой локации может появиться босс: Робот-Блогер Финальный с ракетницей

  • Трубочных и двуполых Собутыльников нарисовать сложно, но в идеале было бы рассадить их по камерам Централа четвертой локации.

  • Можно добавить 2D-карту с врагами (внизу и по центру экрана)

Планов полно, но без скелетной анимации они бессмысленны, конечно

Но хватит лирики, перейдем к техническим решениям и собственно коду.

Конфигурация

Особенный кайф от написания кастомной игры в том, что после того как вы доставили новые фичи или любые изменения в код вам просто необходимо расслабиться и их честно искренне протестировать. Ручками. Сделать несколько каток, по любому. Тесты тут никак и ничем не помогут, даже, убежден, наоборот будут мешать прогрессу, особенно если вы не работаете по заранее известному плану, а постоянно экспериментируете. Браузерная игра на джаваскрипт это в принципе превосходный пример того, когда статическая типизация или разработка через тестирование будут только мешать добиться действительно качественного результата. (А на чем тут необходимо проверять типы, господа сеньоры? Я до сих пор в замешательстве от React c CSS Modules и просто Flow, а не TS даже в котором авторы маниакально проверяли что каждый, еще и передаваемый по цепочке компонент, класс модулей для оформления !!! это string А тут что будем маниакально типизировать, вектора?). И даже сам Роберт Мартин в Идеальном программисте делает несколько пассажей на тему бессмысленности TDD, когда говорит о рисках при разработке GUI. В моей игре можно сказать что и нет практически ничего кроме тонны двумерного и трехмерного GUI, ну и логики для него. Любая ошибка либо вызовет исключение, либо неправильное поведение во вьюхе и геймплее, которое может быть очень быстро обнаружено с помощью визуальной проверки, но очень сомнительно что вообще способно быть покрыто тестом.

На самом деле, создавая любую программу или даже просто разметку для программы вы всегда непреложно должны следовать некоторым общеизвестным простым принципам (но не всегда это TDD). Чтобы не происходило укороченный нереальный дедлайн, термоядерная перестрелка за окном вы должны писать свой код таким образом, чтобы его можно было легко отлаживать и развивать. Любое важное или повторяющиеся больше одного раза значение должно быть отражено в конфигурации. Влияющие на геймплей параметры должны быть локализованы в одном файле балансируя геймплей работать вы должны именно и практически только с ним одним.

Все настройки настройки и значения влияющие на геймплей и дизайн (константа DESIGN), а также весь текстовый контент-переводы у меня сосредоточены в constants.js.

Контрол

На сайте библиотеки Three представлено большое количество полезных примеров с демо-стендами, самых разных реализаций, функциональностей которые стоит изучить и по возможности к месту использовать. Я отталкивался в своих исследованиях, прежде всего, вот от этого примера. Это правильный, мягкий инерционный контрол от первого лица который математически обсчитывает столкновения с клеткой-миром gld-моделью с помощью октодерева. Проверять столкновения можно для капсулы (для героя или врагов) или обычных сферы Sphere и луча Ray от Three. Этого в принципе достаточно для чтобы сделать FPS-игру: сделать так чтобы герой и враги не сталкивались с миром и между собой, выстрелы взрывались при попадании в другие объекты и тд.

Для того чтобы понимать что происходит когда вы нажимаете кнопку Играть, игра запускается и курсор мыши пропадает вы должны знать о браузерной фиче Pointer_Lock_API. Мы добавляем такой контрол вместе со всей стандартной кухней Three в инициализации основного компонента-сцены, ищите:

// Controls// In First Person...

Но! Тут нюанс браузеры обязательно оставляют путь для панического отступления пользователю и резервируют клавишу Esc для того чтобы пользователь всегда мог разлочить указатель. Это касается нашего UI/UX в игре необходима клавиша P ставящая мир на паузу. Когда указатель залочен то бишь запущен игровой процесс нажатие на Esc, как уже сказано вызовет паузу. Но если мы попытаемся добавить обработку отпускания по 27ому коду даже только для режима паузы, все равно очень быстро увидим в консоли:

ОшибкаОшибка

Поэтому: забудьте про Esc. Пауза по клавише P. Есть еще одно ограничение и проблема связанная с созданием хорошего FPS-контрола: оружие. Я так понял что в энтерпрайзных реализациях руки-оружие это отдельный независимый план наложенный поверх мира. С Three, насколько я понимаю, сделать так не получится. Поэтому мой пока единственный в арсенале грозный виномет с оптическим прицелом это объект сцены который приделан к контролу. Я копирую вектор направления камеры на него. Но около зенита и надира в результате его начинает штормить он не может однозначно определить позицию. При взгляде совсем под ноги я его просто скрываю, а вот стрелять наверх нужно. Что делать с этим небольшим и не особо заметным багом я пока не придумал.

Оптический прицел винометаОптический прицел винометаВыстрел вверхВыстрел вверх

Пытаясь сделать скоростной задорный шутер на Three мы можем сразу забыть о тенях или дополнительных источниках освещения, особенно движущихся. Да, я пытался запилить качающиеся на ветру лампы для особенного мрачняка и криповости, движущиеся тени от них. Нет никак нельзя даже статичные точечные источники света сильно просаживают производительность (а нам еще врагов гонять). По поводу света я пришел к простому компромиссу: чтобы картинка не выглядела совсем сухо и скучно приделать мощный фонарик к контролу, герою. Фонарик можно выключать клавиша T.

Далее я просто пройдусь по основным модулям давая небольшие комментарии в интересных моментах.

Сцена

Основной компонент Scene.vue предоставляет:

  • всю стандартную кухню Three: Renderer, Scene и ее туман, Camera и Audio listener в ней, Controls

  • набор утилитарных переменных для использования в анимационных циклах низовых модулей

  • переменные для хранения коллекций примитивных дополнительных объектов превдоmesh`ей по которым работает кастинг

  • в том числе и через используемые миксины все необходимые ему самому или его низовым модулям геттеры и экшены стора Vuex

  • обрабатывает большинство (кроме тех, что удобно ловить в логике героя) событий клавиатуры, мыши и так далее

  • инициализирует Аудиошину, Шину Событий и Мир

  • анимирует Шину Событий, Героя и Мир

  • в наблюдателях значений важных геттеров добавляет игровой логики

Весь код тут простой, прямо очевидный, практически не требующий дополнительных пояснений. Что-то мы будем рассматривать дальше, например что за такие превдоmesh`и для кастинга. Но стоит только остановить внимание на такой простой базовой сущности как переменные. Дело в том что существует еще один действительно важный аспект который позволит нам вытянуть эту задачу с шутером это тут можно начинать смеяться экономия памяти в джаваскрипте (господа техлиды-сеньоры?). Да не надо ни при каких обстоятельствах создавать переменные в анимационных циклах и проверках низовых модулей, нужно использовать только те что уже есть в основном компоненте (ну или модуле), созданы заранее. Контекст основного компонента можно передавать в публичные методы низовых модулей-функций.

Стандартный модуль героя, врагов, предмета или специфического объекта вроде двери или информационной панели в общем виде выглядит так:

import * as Three from 'three';import { DESIGN } from '@/utils/constants';function Module() {  let variable; // локальная переменная - когда очень удобна или необходима при инициализации или во всей логике    // ...  // Инициализация  this.init = (    scope,    texture1,    material1,    // ...  ) => {    // variable = ...    // ...  };  // Функция анимационного цикла для этого модуля - опционально (предметы, например, не нужно анимировать)  this.animate = (scope) => {    // А вот тут и в остальной логике стараемся использовать уже только переменные Scene.vue:    scope.moduleObjectsSore.filter(object => object.mode === DESIGN.ENEMIES.mode.active).forEach((object) => {      // scope.number = ...      // scope.direction = new Three.Vector3(...);      // variable = ... - так, конечно, тоже можно, главное не let variableNew;      // ...    });  };}export default Module;

Стор

Хранилище Vuex поделено на 3 простых модуля. layout.js отвечает за основные параметры игрового процесса: паузы-геймоверы и тд, взаимодействует с API-бекенда. В hero.js большое количество полей и их геттеров, но всего два экшена/мутации. Этот модуль позволяет в максимально унифицированной форме распространять изменения значений отдельных параметров, шкал, флагов на герое с помощью setScale или может пакетно установить эти значения через setUser.

Третий модуль совсем примитивный preloader.js и целиком состоит из однотипных boolean-полей с false по дефолту. Пока его поле isGameLoaded единственное в состоянии модуля с геттером с false не получает true при запуске или перезагрузке приложения пользователь будет видеть лоадер. Каждое из остальных полей обозначает подгрузку определенного ассета: текстуры, модели, аудио или постройку определенного типа объектов.

Если нам нужно подгрузить, например, текстуру песка:

import * as Three from 'three';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  this.init = (    scope,    // ...  ) => {    const sandTexture = new Three.TextureLoader().load(      './images/textures/sand.jpg',      () => {        scope.render(); // нужно вызвать рендер если объекты использующию эту текстуру заметны "на первом экране"          loaderDispatchHelper(scope.$store, 'isSandLoaded');      },    );  };}export default Module;
// В @/utils/utilities.js:export const loaderDispatchHelper = (store, field) => {  store.dispatch('preloader/preloadOrBuilt', field).then(() => {    store.dispatch('preloader/isAllLoadedAndBuilt');  }).catch((error) => { console.log(error); });};

Когда отправка сообщения о том что элемент подгружен разрешается функция-помощник из набора атомов-утилит отправляет экшен проверяющий все ли готово?.

Согласен что решение по прелоадера не идеальное с точки зрения UI в том смысле что мы не демонстрируем общий прогресс по загрузке. Но на данном этапе это не кажется критически важным, особенно в свете озвученной выше концепции и даже факта быстрой перезагрузки локаций.

Аудиошина

Одна только необходимость воздействовать сразу на все звучащие аудио, например, при переходе в режим паузы или при включении машины времени диктует требование формирование общего микшера в системе аудиошины. Кроме того, такой подход максимально удобно унифицирует синтаксис однообразных похожих вызовов и избавляет от необходимости следить за очередностью подгрузки аудио на объекты (когда на одном объекте может звучать несколько) с помощью LoadingManager`ов.

Аудио бывают:

1) Звучащие на контроле-герое и PositionalAudio на объектах

2) Луп или сэмпл

Насколько я понял аудио-API Three полностью отражает API браузера. Меня как человека уже всю осознанную жизнь занимающемуся на досуге звукорежиссурой несколько напрягает необходимость станцевать с бубном в коде для того если захочется добавить простой фейд в начале или конце сэмпла, но что тут поделаешь. При желании мы как раз можем удобно добавить такую функциональность именно с помощью модуля аудиошины.

В Hero удобно записывать аудио в переменную чтобы можно было просто работать [в обход шины] с ними в специфической логике:

// В @/components/Three/Scene/Hero.js:import * as Three from "three";import {  DESIGN,  // ...} from '@/utils/constants';import {  loaderDispatchHelper,  // ...} from '@/utils/utilities';function Hero() {  const audioLoader = new Three.AudioLoader();  let steps;  let speed;  // ...  this.init = (    scope,    // ...  ) => {    audioLoader.load('./audio/steps.mp3', (buffer) => {      steps = scope.audio.addAudioToHero(scope, buffer, 'steps', DESIGN.VOLUME.hero.step, false);      loaderDispatchHelper(scope.$store, 'isStepsLoaded');    });  };  this.setHidden = (scope, isHidden) => {    if (isHidden) {      // ...      steps.setPlaybackRate(0.5);    } else {      // ...      steps.setPlaybackRate(1);    }  };  this.setRun = (scope, isRun) => {    if (isRun && scope.keyStates['KeyW']) {      steps.setVolume(DESIGN.VOLUME.hero.run);      steps.setPlaybackRate(2);    } else {      steps.setVolume(DESIGN.VOLUME.hero.step);      steps.setPlaybackRate(1);    }  };  // ...  this.animate = (scope) => {    if (scope.playerOnFloor) {      if (!scope.isPause) {        // ...        // Steps sound        if (steps) {          if (scope.keyStates['KeyW']            || scope.keyStates['KeyS']            || scope.keyStates['KeyA']            || scope.keyStates['KeyD']) {            if (!steps.isPlaying) {              speed = scope.isHidden ? 0.5 : scope.isRun ? 2 : 1;              steps.setPlaybackRate(speed);              steps.play();            }          }        }      } else {        if (steps && steps.isPlaying) steps.pause();        // ...      }    }  };}export default Module;

Казалось бы шаги нужно делать лупом? Ан нет, не получится. И, например, вешать еще один последний шаг на окончание движения не позволит само устройство инерционного контрола собирающего много быстрых событий. С ним даже не особо ясно когда это вообще происходит когда позиция героя перестаёт изменяться нам уже не надо. Найденное мною решение простое и хорошо работает, дает нужный эффект. Сэмпл шага это ровно два шага. Если появляется событие клавиатуры запускаем аудио. И оно всегда отыгрывает до конца. Если персонаж переходит на бег или начинает ползти просто меняем скорость.

Тут надо упомянуть только об одном обнаруженном мною нюансе. Если аудио не луп с ним возникают проблемы при постановке и снятии с паузы. При следующих запусках отыгранная в предыдущий раз часть трека съедается пока он вообще не перестает звучать. Фиксит это данная строчка в функции добавления аудио на шину:

if (!isLoop) audio.onEnded = () => audio.stop();

Имейте ввиду!

import * as Three from "three";import { DESIGN, OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  const audioLoader = new Three.AudioLoader();  // ...  let material = null;  const geometry = new Three.SphereBufferGeometry(0.5, 8, 8);  let explosion;  let explosionClone;  let boom;  this.init = (    scope,    fireMaterial,    // ...  ) => {    // Звук наземных врагов - загружаем в инициализации на объекты через шину    audioLoader.load('./audio/mechanism.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isMechanismLoaded');      scope.array = scope.enemies.filter(enemy => enemy.name !== OBJECTS.DRONES.name);      scope.audio.addAudioToObjects(scope, scope.array, buffer, 'mesh', 'mechanism', DESIGN.VOLUME.mechanism, true);     });    // Звук взрыва - то есть - "добавляемой и уничтожаемой" сущности - загружаем и записываем в переменную    material = fireMaterial;    explosion = new Three.Mesh(geometry, material);    audioLoader.load('./audio/explosion.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isExplosionLoaded');      boom = buffer;    });  };  // ...  // ... где-то в логике врагов:  this.moduleFunction = (scope, enemy) => {    scope.audio.startObjectSound(enemy.id, 'mechanism');    // ...    scope.audio.stopObjectSound(enemy.id, 'mechanism');    // ...  };  // При добавлении взрыва на шину взрывов:  this.addExplosionToBus = (    scope,    // ...  ) => {    explosionClone = explosion.clone();    // ..    scope.audio.playAudioOnObject(scope, explosionClone, boom, 'boom', DESIGN.VOLUME.explosion);    // ..  };}export default Module;

Попробуйте подобрать и принять зеленый цветок запускающий машину времени в игре, классно? ))

Шина событий и сообщения

Очевидно что кроме аудиомикшера игре необходима еще одна одна шина: делей для событий модуль который будет задерживать отправку изменений в контексте игрового времени. Например, если было показано сообщение, а потом система поставлена на паузу мы не можем использовать нигде обычные таймауты только обновляемые в анимационном цикле часы Clock Three. Модуль хранит актуальные записи о связанных событиях и в нужный момент вызывает переданный коллбэк.

В игре присутствует интерфейс текстовых сообщений которые показываются в правом верхнем углу экрана. Сообщения могут быть двух видов: постоянные и временные. Первые должны быть видимы, например, когда герой кастит предмет или дверь и игра сообщает об этом. Вторые сообщают о состоявшихся игровых событиях, нумеруются и должны показываться в виде очереди на короткий интервал времени каждое. Для них категорически необходима функциональность шины событий.

Мир

Модель первой локацииМодель первой локации

В инициализации модуля мира по порядку:

  1. Загружаются все переиспользуемые в остальных модулях текстуры и создаются все такие материалы и геометрии.

  2. Загружается и разбирается модель уровня. На ее основе формируются массивы данных обо всех игровых объектах в OBJECTS и рабочие массивы, списки псевдообъектов для сущностей с одинаковой функциональностью в основном контексте.

  3. На примитивы которые должны стать видимыми элементами, строительными блоками мира накладываются текстуры. Места-пустышки обозначающие места рождения врагов или нахождения полезных предметов после учета удаляются.

  4. Двери переносятся в отдельную группу из основной модели. Из этих двух групп очищенной сцены и все дверей создаются стартовые октодеревья.

  5. Инициализируются все остальные модули.

Я разбираю один файл glb и как совершенно необходимый такой игре редактор уровней и как готовую модель для построения стартовых октодеревьев мира, и, отдельно дверей в нем, и как почти готовую не текстурированную основу самого примитивного визуального мира. Различать примитивы можно с помощью специфических маркеров в их наименовании. Это не самое надежное соглашение, оно чревато ошибками, но они легко обнаруживаются визуально при ручном тестировании. Изменения можно вносить очень быстро. Тут уже все зависит от вашей фантазии и выдуманного с помощью нее дизайна и геймплея, ну и количества времени которые вы можете на это потратить. Например, я использую маркер Mandatory если хочу чтобы цветок или бутылка были обязательными, если его нет постройка зависит от рандома. Или для механики включения-выключения информационных панелей собирается отдельный массив с их комнатами параллелепипедами определяющими объем в котором панель реагирует на персонажа. Для геометрии такого объекта следует сделать при инициализации:

room.geometry.computeBoundingBox();

room.visible = false;

И теперь у нас может быть вот такой публичный метод в модуле панелей и его забегая вперед можно будет использовать в модуле Атмосферы при проверках отношения панелей к герою:

// В @/components/Three/Scene/World/Screens.js:this.isHeroInRoomWithScreen = (scope, screen) => {scope.box.copy(screen.room.geometry.boundingBox).applyMatrix4(screen.room.matrixWorld); if (scope.box.containsPoint(scope.camera.position)) return true;return false;};

При постройке дверей я использую примитив из основного файла как массив самой двери, добавляя дизайн уже в джаваскрипте текстуру, маркеры уровня доступа с обеих сторон и псевдоmesh. Для правильной работы дверей все равно нужен некоторый такой дополнительный псевдообъем и подобная комнатам панелей логика для того чтобы не дверь не закрывалась когда герой находится в ее проеме и отскакивала если зашел в него во время закрытия.

Псевдообъект-помощник для двериПсевдообъект-помощник для двериДверь не закрываетсяДверь не закрывается

Совершенно точно что тут можно улучшить сделать пустышки обозначающие место рождения врагов не планами один на один метр а большими планами для наземных юнитов и трехмерными коробками для воздушных чтобы они могли появляться в случайной точке некоторого прямоугольника на поверхности или трехмерного объема. Но пока лень. )

Анимационный цикл мира это просто список анимационных циклов всех модулей врагов, объектов и оружия которые должны быть анимированы кроме героя. Если включена машина времени то он вызывается через раз.

Кастинг

Вот мы и добрались до самого интересного: кастинг и столкновения. Как сделать так, чтобы предметы можно было собирать, а герой и враги не сталкивались с миром и друг-другом. Для обоих механик я использую дополнительные невидимые примитивы, псевдоmesh`и. Они инициализируются и записываются в абстрактные объекты которыми оперирует система вместе с основным видимым и всеми необходимыми им флагами-свойствами. Для движущихся врагов еще записывается коллайдер Sphere. Псевдомеши идут на кастинг (предметы) или построение и обновление октодеревьев (враги). А коллайдер для проверки столкновения с ними.

Псевдообъекты-помощники для предметовПсевдообъекты-помощники для предметов

Геометрия и материал готовиться в мире перед инициализацией всех вещей и надежнее сделать материал двусторонними так кастинг будет работать даже если герой оказался внутри псевдообъекта:

// В @/components/Three/Scene/World.js:const pseudoGeometry = new Three.SphereBufferGeometry(DESIGN.HERO.HEIGHT / 2,  4, 4); const pseudoMaterial = new Three.MeshStandardMaterial({ color: DESIGN.COLORS.white, side: Three.DoubleSide,});new Bottles().init(scope, pseudoGeometry, pseudoMaterial);

В модуле конкретной вещи:

// В @/components/Three/Scene/World/Thing.js:import * as Three from 'three';import { GLTFLoader } from '@/components/Three/Modules/Utils/GLTFLoader';import { OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Thing() {  let thingClone;  let thingGroup;  let thingPseudo;  let thingPseudoClone;  this.init = (    scope,    pseudoGeometry,    pseudoMaterial,  ) => {    thingPseudo = new Three.Mesh(pseudoGeometry, pseudoMaterial);    new GLTFLoader().load(      './images/models/Objects/Thing.glb',      (thing) => {        loaderDispatchHelper(scope.$store, 'isThingLoaded'); // загружена модель        for (let i = 0; i < OBJECTS.THINGS[scope.l].data.length; i++) {          // eslint-disable-next-line no-loop-func          thing.scene.traverse((child) => {            // ... - тут "покраска" материалами частей вещи          });          // Клонируем объект и псевдо          thingClone = thing.scene.clone();          thingPseudoClone = thingPseudo.clone();          // Псевдо нужно дать правильное имя чтобы мы могли различать его при кастинге          thingPseudoClone.name = OBJECTS.THINGS.name;          thingPseudoClone.position.y += 1.5; // корректируем немного позицию по высоте          thingPseudoClone.visible = false; // выключаем рендер          thingPseudoClone.updateMatrix(); // обновляем          thingPseudoClone.matrixAutoUpdate = false; // запрещаем автообновление          // Делаем из обхекта и псевдо удобную группу          thingGroup = new Three.Group();          thingGroup.add(thingClone);          thingGroup.add(thingPseudoClone);          // Выставляем координаты из собранных из модели уровня данных          thingGroup.position.set(            OBJECTS.THINGS[scope.l].data[i].x,            OBJECTS.THINGS[scope.l].data[i].y,            OBJECTS.THINGS[scope.l].data[i].z,          );          // Записываем в "рабочие объеты" - по ним будем кастить и прочее          scope.things.push({            id: thingPseudoClone.id,            group: thingGroup,          });          scope.objects.push(thingPseudoClone);          scope.scene.add(thingGroup); // добавляем на сцену        }        loaderDispatchHelper(scope.$store, 'isThingsBuilt'); // построено      },    );  };}export default Thing;

Теперь мы можем тыкать направленным вперед лучом из героя в анимационном цикле Hero.js:

// В @/components/Three/Scene/Hero.js:import { DESIGN, OBJECTS } from '@/utils/constants';function Hero() {  // ...  this.animate = (scope) => {    // ...    // Raycasting    // Forward ray    scope.direction = scope.camera.getWorldDirection(scope.direction);    scope.raycaster.set(scope.camera.getWorldPosition(scope.position), scope.direction);    scope.intersections = scope.raycaster.intersectObjects(scope.objects);    scope.onForward = scope.intersections.length > 0 ? scope.intersections[0].distance < DESIGN.HERO.CAST : false;    if (scope.onForward) {      scope.object = scope.intersections[0].object;      // Кастим предмет THINGS      if (scope.object.name.includes(OBJECTS.THINGS.name)) {        // ...      }    }    // ...  };}export default Hero;

Кастинг очень полезен и для усовершенствования ИИ врагов. С помощью него возможно проверять имеет ли смысл, есть ли возможность двигаться-прыгать вперед, лететь вниз, делать выстрел. В утилитах:

// В @/utils/utilities.js:// let arrowHelper;const fixNot = (value) => { if (!value) return Number.MAX_SAFE_INTEGER; return value;};export const isEnemyCanMoveForward = (scope, enemy) => { scope.ray = new Three.Ray(enemy.collider.center, enemy.mesh.getWorldDirection(scope.direction).normalize()); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); scope.resultEnemies = scope.octreeEnemies.rayIntersect(scope.ray); // arrowHelper = new Three.ArrowHelper(scope.direction, enemy.collider.center, 6, 0xffffff); // scope.scene.add(arrowHelper); if (scope.result || scope.resultDoors || scope.resultEnemies) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance), fixNot(scope.resultEnemies.distance));   return scope.number > 6; } return true;};

Для наглядной визуальной отладки подобных механик очень полезен объект Three ArrowHelper. Если мы включим его добавление на сцену в функции выше:

Отладка с включенными стрелочными помощникамиОтладка с включенными стрелочными помощниками

С помощью подобной простой функции можно добиться того что враги не реагируют на героя через стены строим луч от центра коллайдера к камере и сравниваем дистанцию от камеры до обнаруженного столкновения:

// В @/utils/utilities.js:export const isToHeroRayIntersectWorld = (scope, collider) => { scope.direction.subVectors(collider.center, scope.camera.position).negate().normalize(); scope.ray = new Three.Ray(collider.center, scope.direction); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); if (scope.result || scope.resultDoors) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance));   scope.dictance = scope.camera.position.distanceTo(collider.center);   return scope.number < scope.dictance; } return false;};

Враги

Кроме инициализации самих низовых модулей врагов, модуль Enemies.js содержит и всю общую логику обслуживающую жизненный цикл врагов. Сейчас у меня четыре этапа-режима жизни у врагов:

// В @/utils/constatnts.js:export const DESIGN = {  DIFFICULTY: {    civil: 'civil',    anarchist: 'anarchist',    communist: 'communist',  },  ENEMIES: {    mode: {      idle: 'idle',      active: 'active',      dies: 'dies',      dead: 'dead',    },    spider: {      // ...      decision: {        enjoy: 60,        rotate: 25,        shot: {          civil: 40,          anarchist: 30,          communist: 25,        },        jump: 50,        speed: 20,        bend: 30,      },    },    drone: {      // ...      decision: {        enjoy: 50,        rotate: 25,        shot: {          civil: 50,          anarchist: 40,          communist: 30,        },        fly: 40,        speed: 20,        bend: 25,      },    },  },  // ...};
// В @/components/Three/Scene/Enemies.js:import { DESIGN } from '@/utils/constants';import {  randomInteger,  isEnemyCanShot,  // ...} from "@/utils/utilities";function Enemies() {  // ...  const idle = (scope, enemy) => {    // ...  };  const active = (scope, enemy) => {    // ...    // Где-то в логике агрессивного режима: решение на выстрел (если отдыхает)    scope.decision = randomInteger(1, DESIGN.ENEMIES[enemy.name].decision.shot[scope.difficulty]) === 1;    if (scope.decision) {      if (isEnemyCanShot(scope, enemy)) {        scope.boolean = enemy.name === OBJECTS.DRONES.name;        scope.world.shots.addShotToBus(scope, enemy.mesh.position, scope.direction, scope.boolean);        scope.audio.replayObjectSound(enemy.id, 'shot');      }    }  };  const gravity = (scope, enemy) => {    // ...  };  this.animate = (scope) => {    scope.enemies.filter(enemy => enemy.mode !== DESIGN.ENEMIES.mode.dead).forEach((enemy) => {      switch (enemy.mode) {        case DESIGN.ENEMIES.mode.idle:          idle(scope, enemy);          break;        case DESIGN.ENEMIES.mode.active:          active(scope, enemy);          break;        case DESIGN.ENEMIES.mode.dies:          gravity(scope, enemy);          break;      }    });  };}export default Enemies;

В зависимости от вспомогательных проверочных функций которые мы затрагивали выше, своего состояния (на полу или в прыжке, например, если это наземный юнит) каждый живой враг может с некоторой частотой принимать определенные решения.

Но! Самое важное на что нужно обратить внимание: в idle спокойном режиме полноценно двигается некоторое случайное время только один выбранный случайным образом враг. Остальные поворачиваются на месте + может и должна быть запущена анимация. Такая оптимизация позволяет действительно полноценно разгрузить систему.

Столкновения

Октодеревом в данном тексте обозначается максимально упрощенная модель 3D-пространства которое занимает некоторая группа объектов с минимально необходимым и достаточным для обсчета количеством граней, рёбер и вершин.

Такие октодеревья, как вы уже наверняка поняли из примера полезной функции из конца раздела про кастинг помогают нам грамотно обсчитывать кастинг лучей или столкновения коллайдеров героя/врагов с миром и другими объектами. В случае если персонаж движется и мы обнаруживаем столкновение октодерева с коллайдером капсулой героя или сферой врага вектору ускорения этого объекта добавляется вектор выталкивающий обратно центр его коллайдера (и все остальное, соответственно).

В текущей реализации используются три октодерева: мир: 1) пол, бетонные блоки, трубы, стекла, а также 2) двери и 3) враги. Каждый из врагов обсчитывает свои столкновения с персональным октодеревом врагов собранным без него.

Мы можем и должны обновлять октодеревья. Если дверь была открыта-закрыта или враг передвинулся нам нужно пересобрать соответствующее октодерево. При открытии закрытии двери лучше всего сделать это два раза: когда дверь открылась или закрылась настолько что герой уже может или наоборот больше не может пройти через нее и когда процесс открытия/закрытия завершен.

В моей реализации на объекты врагов инициализируется по две псевдо-коробки. Это не оптимально, но сделано в целях балансировки визуальной составляющих двух механик: 1) столкновение героя с врагами 2) столкновения врагов с миром и между собой. Визуальное тестирование показало что для реализации первой механики нужна коробка намного более меньшего размера чем для второй.

Точность обсчета столкновений с октодеревьями напрямую зависит от качества оптимизации анимационного цикла и ее непосредственного результата фактической производительности системы. Если скорость объекта высокая, но система начинает захлебываться, то есть фрейм, дельта анимационного цикла удлиняется обсчет с октодеревом не успеет компенсировать или даже вообще зарегистрировать столкновение и пользователь может отхватить глюк: герой или враг может быть вытолкнут через стену-стекло. Но не будем о грустном. )

Мы не можем обновлять октодерево врагов героя или персональные октодеревья врагов каждый раз когда вызывается обсчет столкновений наша система просто сразу ляжет в таком случае. Но достаточно делать это, например, каждые полсекунды. Обновили октодерево всех врагов для героя запускаем таймер на полсекунды. Точно также и с персональными октодеревьями врагов.

// В @/utils/constatnts.js:export const DESIGN = {  OCTREE_UPDATE_TIMEOUT: 0.5,  // ...};
// В @/utils/utilities.js:// Обновить персональное октодерево врагов для одного врагаimport * as Three from "three";import { Octree } from "../components/Three/Modules/Math/Octree";export const updateEnemiesPersonalOctree = (scope, id) => {  scope.group = new Three.Group();  scope.enemies.filter(obj => obj.id !== id).forEach((enemy) => {    scope.group.add(enemy.pseudoLarge);  });  scope.octreeEnemies = new Octree();  scope.octreeEnemies.fromGraphNode(scope.group);  scope.scene.add(scope.group);};
// Столкновения враговconst enemyCollitions = (scope, enemy) => {  // Столкновения c миром - полом, стенами, стеклами и трубами  scope.result = scope.octree.sphereIntersect(enemy.collider);  enemy.isOnFloor = false;  if (scope.result) {    enemy.isOnFloor = scope.result.normal.y > 0;    // На полу?    if (!enemy.isOnFloor) {      enemy.velocity.addScaledVector(scope.result.normal, -scope.result.normal.dot(enemy.velocity));    } else {      // Подбитый враг становится совсем мертвым после падения на пол и тд      // ...    }    enemy.collider.translate(scope.result.normal.multiplyScalar(scope.result.depth));  }  // Столкновения c дверями  scope.resultDoors = scope.octreeDoors.sphereIntersect(enemy.collider);  if (scope.resultDoors) {    enemy.collider.translate(scope.resultDoors.normal.multiplyScalar(scope.resultDoors.depth));  }  // Делаем октодерево из всех врагов без этого, если давно не делали  if (scope.enemies.length > 1    && !enemy.updateClock.running) {    if (!enemy.updateClock.running) enemy.updateClock.start();    updateEnemiesPersonalOctree(scope, enemy.id);    scope.resultEnemies = scope.octreeEnemies.sphereIntersect(enemy.collider);    if (scope.resultEnemies) {      result = scope.resultEnemies.normal.multiplyScalar(scope.resultEnemies.depth);      result.y = 0;      enemy.collider.translate(result);    }  }  if (enemy.updateClock.running) {    enemy.updateTime += enemy.updateClock.getDelta();    if (enemy.updateTime > DESIGN.OCTREE_UPDATE_TIMEOUT && enemy.updateClock.running) {      enemy.updateClock.stop();      enemy.updateTime = 0;    }  }};

Своя атмосфера

Модуль с романтическим названием Atmosphere.js отвечает за элементы мира которые одинаковы для всех локаций: свет, небо, и в дальнейшем взаимодействие между другими модулями в анимационном цикле работает посредником.

Если вывалится за стену и забежать за край небаЕсли вывалится за стену и забежать за край неба

Я долго придумывал и придумал как сделать прикольное небо, облака: это огромная вывернутая по одной из осей и вращающаяся текстурированная сфера.

При некотором изменении позиции героя (сейчас это 10 метров) модуль проверяет всех живых врагов и панели и меняет их режимы в зависимости от актуальной ситуации. Сейчас дроны не видят героя через толстые пуленепробиваемые стекла. Для того чтобы это изменить нужно выделить стекла в отдельное октодерево, как и двери.

Пуленепробиваемые стеклаПуленепробиваемые стекла

Да, это вам не React c TS и тестами в финтех и банки!

Выводы которые я могу сделать на основе практики создания браузерной FPS на Three:

  • Мы не можем использовать тени и множество источников света

  • Мы должны экономить память в анимационном цикле и использовать в нем только готовые переменные

  • Мы должны и во всех остальных возможных аспектах максимально тщательно оптимизировать анимационный цикл, кастинг и обсчет столкновений в нем в контексте геймплея, так, чтобы сохранить драйв, но избежать падения производительности

  • Статическая типизация и юнит-тесты ничем не могут помочь в данном эксперименте

В принципе, я доволен тем что сейчас уже получилось. И хочется довести это до полной красоты. Поэтому если вы знаете кого-то кто увлекается скелетной анимацией и может согласится добавить несколько простых треков на мои glb скиньте, пожалуйста ему ссылку на статью?

Подробнее..

Перевод Размытие фона в Google Meet с расчётом ML в браузере

09.11.2020 18:08:51 | Автор: admin
Видеоконференции занимают всё большее место в работе и личной жизни людей. Чем лучше организован процесс с точки обработки видео и приватности, тем меньше отвлекается внимание человека от самой конференции. В рамках этой задачи мы недавно разработали фильтры размытия и замены фона в Google Meet. Они задействуют машинное обучение (ML) для лучшего выделения участников независимо от их окружения. В то время как другие решения требуют установки дополнительного программного обеспечения, функции Meet основаны на передовых технологиях Web ML, построенных с помощью MediaPipe, и работают непосредственно в вашем браузере никаких дополнительных шагов не требуется. Одной из ключевых целей при разработке этих функций было обеспечение производительности в реальном времени в браузере почти на всех современных устройствах, чего мы достигли, объединив эффективные модели ML на устройстве, рендеринг на основе WebGL и вывод ML на веб-стеке с помощью XNNPACK и TFLite.


Размытие и замена фона на основе MediaPipe

Обзор нашего решения Web ML


Новые функции Meet разработаны с помощью MediaPipe, опенсорсной платформы Google для потоковых трансляций. На этом фреймворке основаны другие ML-решения, такие как отслеживание руки, радужной оболочки глаза и положения тела в реальном времени.

Главным требованием любой мобильной технологии является достижение высокой производительности. Для этого веб-конвейер MediaPipe применяет низкоуровневый бинарный формат WebAssembly, разработанный специально для веб-браузеров и повышающий скорость выполнения сложных вычислительных задач. Во время выполнения браузер преобразует инструкции WebAssembly в машинный код, который выполняется намного быстрее, чем традиционный код JavaScript. Кроме того, в Chrome84 недавно реализовали поддержку WebAssembly SIMD, где каждая инструкция обрабатывает несколько точек данных, что увеличивает производительность более чем в два раза.

Наше решение сначала обрабатывает каждый видеокадр, отделяя пользователя от фона (подробнее о модели сегментации см. ниже), используя вывод ML для вычисления маски низкого разрешения. При необходимости мы дополнительно уточняем маску, чтобы выровнять её с границами изображения. Затем маска используется для рендеринга видеовыхода через WebGL2, а фон размывается или заменяется.


Конвейер WebML: все вычислительные операции реализованы на C++/OpenGL и выполняются в браузере через WebAssembly

В текущей версии расчёт выполняется на CPU клиента с низким энергопотреблением и самым широким охватом устройств. Для достижения высокой производительности в реальном времени мы разработали эффективные модели ML с использованием библиотеки XNNPACK для ускорения расчёта. Это первый механизм видеовывода, специально разработанный для новой спецификации WebAssembly SIMD. За счёт ускорения XNNPACK и SIMD модель сегментации работает в режиме реального времени в интернете.

Благодаря гибкой конфигурации MediaPipe решение для размытия/замены фона адаптирует свою обработку в зависимости от возможностей устройства. На устройствах высокого класса запускается полный конвейер для обеспечения высочайшего визуального качества, в то время как на устройствах низкого класса он переключается на облегчённые вычислительные модели ML, без уточнения маски.

Модель сегментации


Модели ML на устройстве должны быть сверхлёгкими для быстрого расчёта, низкого энергопотребления и небольшого размера. Для моделей в браузере входное разрешение сильно влияет на количество операций с плавающей запятой (FLOPS), необходимых для обработки каждого кадра, и поэтому оно также должно быть небольшим. Прежде чем подавать изображение в модель, мы его уменьшаем. Восстановление максимально точной маски из картинки с низким разрешением усложняет проектирование модели.

Общая сеть сегментации имеет симметричную структуру кодирования/декодирования. Слои декодера (светло-зелёный) симметричны слоям кодера (светло-синий). В частности, канальное внимание (channel-wise attention) с пулингом по средней величине (global average) применяется как в блоках кодера, так и в блоках декодера, что снижает нагрузку на CPU.


Архитектура модели с кодером MobileNetV3 (светло-синий) и симметричным декодером (светло-зелёный)

Для кодера мы модифицировали нейросеть MobileNetV3-small, дизайн которой спроектирован автоматически методом поиска сетевой архитектуры для достижения наилучшей производительности на слабом железе. Чтобы вдвое уменьшить размер модели, мы экспортировали её в TFLite с квантованием float16, что привело к небольшой потере точности, но без заметного влияния на качество. В полученной модели 193тыс. параметров и размер всего 400КБ.

Эффекты рендеринга


После сегментации мы используем шейдеры OpenGL для обработки видео и рендеринга эффектов. Задача состоит в эффективной визуализации без артефактов. На этапе уточнения совместный билатеральный фильтр сглаживает маску низкого разрешения.


Подавление артефактов при рендеринге. Слева: совместный двусторонний фильтр сглаживает маску сегментации. Посредине: отдельные фильтры удаляют артефакты с ореолами. Справа: замена фона методом световой обёртки

Шейдер размытия имитирует эффект боке, регулируя силу размытия в каждом пикселе пропорционально значениям маски сегментации, подобно пятну рассеяния в оптике. Пиксели взвешиваются по радиусам пятен, так что пиксели переднего плана не будут перетекать в фон. Мы внедрили разделяемые фильтры для взвешенного размытия вместо популярной гауссовой пирамиды, поскольку они удаляют артефакты ореола вокруг человека. Для производительности размытие выполняется с низким разрешением и смешивается с входным кадром в исходном разрешении.


Примеры размытия фона

При замене фона используется техника, известная как световая обёртка (light wrapping) для наложения сегментированных лиц на индивидуальный фон. Световая обёртка помогает смягчить края сегментации, позволяя фоновому свету проливаться на элементы переднего плана, увеличива реалистичность композиции. Это также помогает минимизировать артефакты ореола, когда возникает большой контраст между передним планом и фоном.


Примеры замены фона

Производительность


Чтобы оптимизировать работу на различных устройствах, мы предоставляем варианты моделей с несколькими размерами входного изображения (например, 256x144 и 160x96 в текущей версии), автоматически выбирая лучший вариант в соответствии с доступными аппаратными ресурсами.

Мы оценили скорость вывода модели и сквозной конвейер на двух популярных устройствах: MacBook Pro 2018 с 6-ядерным процессором Intel Core i7 2,2 ГГц и Acer Chromebook 11 с процессором Intel Celeron N3060. Для ввода 720p MacBook Pro может запускать более качественную модель со скоростью 120 кадров в секунду и сквозной конвейер со скоростью 70 кадров в секунду, в то время как на Chromebook модель работает со скоростью 62 кадра в секунду с моделью более низкого качества, а сквозной конвейер выдаёт 33FPS.

Модель
FLOPS
Устройство
Вывод модели
Конвейер
256x144
64млн
MacBook Pro 18
8,3мс (120FPS)
14,3мс (70FPS)
160x96
27млн
Acer Chromebook 11
16,1мс (62FPS)
30мс (33FPS)
Скорость вывода модели и сквозной конвейер на ноутбуках высокого (MacBook Pro) и низкого класса (Chromebook)

Для количественной оценки точности модели используются популярные метрики: коэффициент Жакара (intersection-over-union, IOU) и граничная F-мера (boundary F-score). Обе модели демонстрируют высокое качество работы, особенно на такой легковесной сети:

Модель
IOU
Граничная
F-мера

256x144
93,58%
0,9024
160x96
90,79%
0,8542

Мы также выложили в открытый доступ карту модели для сегментации, где подробно указаны спецификации для оценки. В оценочные данные входят изображения из 17 географических субрегионов земного шара с аннотациями для тона кожи и пола. Анализ показал, что модель демонстрирует устойчиво высокий результат на различных регионах, оттенках кожи и полах, с небольшими отклонениями в показателях IOU.

Вывод


Таким образом, мы представили новое браузерное решение ML для размытия и замены фона в Google Meet. Благодаря этому решению модели ML и шейдеры OpenGL показывают эффективную работу в интернете. Разработанные функции обеспечивают производительность в реальном времени при низком энергопотреблении даже на маломощных устройствах.
Подробнее..

OpenCASCADE и Невидимое солнце Дао

09.09.2020 18:05:36 | Автор: admin

Тот лучший путник, что следов не оставляет
Тот лучший лидер, что без речи вдохновляет
План совершенен, если плана вовсе нет
И если мудрый двери закрывает,
Вам никогда не разгадать секрет.

Великая книга Дао - Стих 27 ( Перевод Ю. Полежаевой)

Привет, Хабр! Хочу сегодня пригласить в увлекательное 3D-путешествие. Мне нравится 3D. И хотя я пробовал работать в разных программах, но меня не покидало чувство, что мне чего-то не хватает. Даже если пользоваться встроенным скриптингом.

Поэтому я постепенно пришел к идее, что для реализации моих безумных творческих идей, наверное лучше подойдут CAD-системы. Вот там есть где разгуляться 3D-фантазии. Мощные алгоритмы создания поверхностей пересечений, проекций, аппроксимаций. Это как раз то, что нужно. Вообще, мне кажется, что разработчики промышленных геометрических CAD-ядер относятся к остальному 3D миру по принципу "Солдат ребенка не обидит".

Конечно, в своих поисках я не мог пройти мимо открытого CAD-ядра OpenCascade. Эта библиотека предоставляет уникальную возможность ближе познакомится с принципами внутреннего устройства современных CAD-систем. Вдвойне приятно, что это можно сделать на дружелюбном языке Python.

От одной мысли о всех этих чудесах, мое сердце начинает биться сильнее. Как поется в одной песне - я приятную дрожь ощущаю с головы до ног. Тех у кого в этот вечер похожее настроение - прошу под кат. Будем рисовать Инь и Янь в объеме.

Главное - поставить сильную задачу

Для того, чтобы испытать CAD-ядро, я решил нарисовать в объеме символ Дао. Какой практический смысл в рисовании древнего китайского символа? Да практически никакого, кроме того, что в процессе рисования потребуются нетривиальные операции и мы сможем проверить, насколько ядро устойчиво ко всяким творческим 3D-махинациям.

Мне в этом смысле понравилась идея, которую я услышал по телевизору от одного астронома. Ведущий его спросил: "Какой практический смысл имеется в астрономии для экономики и народного хозяйства?" Любой ученый от подобных вопросов может легко впасть в депрессию. Но в данном случае астроном не растерялся и ответил, что главный смысл астрономии для народного хозяйства в том, что она ставит перед инженерами по настоящему сильные задачи. Благодаря этому появились сверхчувствительные приемники, сверхточная обработка поверхностей, и много еще чего сверх.

Задача поставлена. И все на что мы можем надеяться - это на древние силы даосизма и на современные силы 3D-моделирования. Как гласит древняя китайская мудрость даже самый далекий и сложный путь начинается с первого шага.

Шаг 1. Настройка среды

Приведу ссылку на разработанный мною документ по настройке OpenCascade в среде Анаконда. Инструкция расcчитана на Win64. Но я думаю, что на Linux можно настроить, не намного сложнее, а может даже и проще.

Установка OpenCascade - Python 3.7 - Win64

Здесь же я оставлю еще несколько ссылок, которые помогут ближе познакомится с OpenCascade

При запуске команд, conda (пакетный менеджер Анаконды) будет пыхтеть и ворчать, считать и пересчитывать зависимости. Когда же он завершит свою нелегкую работу и все нормально запустится, будет ощущение что вам крупно повезло. Возможно так оно и есть.

Шаг 2. Небольшая самодельная библиотека

Чтобы не загромождать код ненужными деталями я написал небольшой набор функций. Ничего выдающегося в нем нет, но я приведу описание этих функций, чтобы было понятно что происходит в примерах.

#initMode = 'screen','web','stl'def ScInit(initMode, decoration, precision, exportDir):  pass#default styles#'stInfo' - for service objects#'stMain' - for main object of drawing#'stFocus' - for important detailsdef ScStyle(styleVal):  pass#draw objectsdef ScPoint(pnt, style):  passdef ScLine(pnt1, pnt2, style):  passdef ScCircle(pnt1, pnt2, pnt3, style):  passdef ScShape(shape, style):  passdef ScLabel(pnt, text, style):  pass#start renderdef ScStart()    

К слову сказать, в библиотеке PythonOCC, кроме непосредственно интерфейса к функциям ядра OpenCascade (cгенерированного с помощью SWIG) понаписано еще много всякого Python-кода, сильно облегчающего жизнь, и за это хочется сказать спасибо тем, кто это сделал.

Шаг 3. Немного о структуре OpenCASCADE

Библиотека OpenCascade неплохо структурирована - в ней все разнесено по уровням. Вначале это кажется излишним, но потом понимаешь, что это разделение вполне логично и полезно. Я бы выделил четыре основных уровня. Чтобы на экране что-то появилось нужно пройти все эти уровни. К счастью, переходы между уровнями довольно легко организованы с помощью различных конструкторов и деструкторов

  1. Математический уровень (линейная алгебра) - точки, вектора, направления, оси, преобразования. Названия пакетов начинаются с gp (что это значит я так и не понял - может geometry primitives)

  2. Геометрический уровень - здесь мы сталкиваемся с различными двухмерными и трехмерными кривыми и поверхностями, задаваемыми различными способами. Названия пакетов начинаются с Geom

  3. Топологический (структурный уровень) - на этом уровне из геометрических объектов, как лоскутное одеяло, сшиваются рабочие объекты. Основные понятия - вершина (vertex), ребро(edge) отрезок кривой или прямой, соединяющий две вершины, контур (wire) - замкнутый набор из ребер, грань (face) - поверхность ограниченная контуром, оболочка (shell) - замкнутый набор граней, ограничивающий некоторый объем, тело (solid) - непосредственно сам объем, ограниченный оболочкой. Согласитесь, что разделение понятий оболочки и тела - граничит с деструктивным педантизмом и во многих 3D-приложениях данное различие просто не принимается во внимание. Здесь же все разложено по полочкам. Топологический уровень - основное отличие библиотек, основанных на граничном представлении объектов (boundary representation), поэтому пакеты данного уровня начинаются с префикса BRep и Topo

  4. Уровень отображения - здесь мы конструируем объекты, которые непосредственно появляются на экране и взаимодействуют с пользователем. Геометрические формы обретают цвет, материал, положение в пространстве, могут быть выбраны мышкой и вообще ведут себя с пользователем очень дружелюбно, за что им был дан префикс AIS).

Еще хотелось бы поделится некоторыми наблюдениями, которые могут вам помочь в работе.

  1. Имя каждого объекта имеет префикс, совпадающий с именем пакета в котором он находится. Это железная необходимость, которая позволяет создателям библиотеки самим не запутаться во всем этом зоопарке. Поэтому при импорте в Python смело пишите в качестве пакета префикс объекта - вы никогда не ошибетесь. Кроме того, зная префикс, можно посмотреть, какие объекты еще есть в пакете. Возможно они подойдут для ваших целей больше, чем тот объект, который вы увидели в примере.

  2. Создатели OpenCascade в плане придумывания имен и сокращений обладают буйной фантазией и имена объектов OpenCascade не пересекаются с именами никаких других библиотек. Поэтому если вы хотите получить справку по объекту - смело вводите его имя в Google. Ссылка на документацию на этот объект окажется в первой строчке выдачи.

  3. Имена и методы объектов не изменяются в зависимости от языка на котором происходит общение с OpenCascade, поэтому вы легко можете использовать примеры и на родном для библиотеки C++, и на ставшем уже экзотикой Tcl, также можно встретить примеры на Java. При должных навыках компьютерного полиглотства все эти примеры легко транслируются в Python.

  4. При использовании объектов будьте внимательны, педантичны и действуйте аккуратно. Очень часто имена объектов и методов отличаются всего на одну букву, а результат их работы отличается кардинально. Я так обжигался несколько раз, и только удача спасала мое психическое здоровье.

Теперь можно приступать к рисованию. Начнем с классики.

Шаг 4. Классические формы Инь и Янь.

Будем использовать принципы параметрического моделирования и единственный параметр который нам потребуется - это r - базовый радиус даосского символа. Как известно символ состоит из большой дуги и двух малых дуг. Я разобью малую дугу головы символа на две дуги. Я это делаю, потому что хочу чтобы в топологии присутствовала точка - вершина символа. Она нам в дальнейшем сильно поможет в построениях.

Первое, что мы сделаем - наметим базовые точки, на которых будет построен нужный нам контур. Базовые точки включают в себя вершины дуг, а также центральные точки для каждой дуги. Построение дуги по трем точкам - это самый удобный способ построения дуги как на плоскости, так и в пространстве. Поверьте мне на слово - если вы задумайте построить дугу каким-то другим способом вы надолго выпадите из культурной, рабочей и семейной жизни.

Вот что получилось (обращаю внимание здесь и далее - это не полный код, а важные для шага процедуры, ссылка на полный код в конце поста):

def getPntsBase(r):        r2 = r/2        gpPntMinC = gp_Pnt(0,r2,0)        p0 = gp_Pnt(0,0,0)          p1 = getPntRotate(gpPntMinC , p0, -pi/4)          p2 = gp_Pnt(-r2,r2,0)          p3 = getPntRotate(gpPntMinC , p0, -pi/4*3)          p4 = gp_Pnt(0,r,0)          p5 = gp_Pnt(r,0,0)          p6 = gp_Pnt(0,-r,0)          p7 = gp_Pnt(r2,-r2,0)              return p0, p1, p2, p3, p4, p5, p6, p7  def getWireDaoClassic(ppBase):        p0, p1, p2, p3, p4, p5, p6, p7  = ppBase        arc1 =  GC_MakeArcOfCircle(p0,p1,p2).Value()    arc2 =  GC_MakeArcOfCircle(p2,p3,p4).Value()    arc3 =  GC_MakeArcOfCircle(p4,p5,p6).Value()    arc4 =  GC_MakeArcOfCircle(p6,p7,p0).Value()     edge1 = BRepBuilderAPI_MakeEdge(arc1).Edge()    edge2 = BRepBuilderAPI_MakeEdge(arc2).Edge()    edge3 = BRepBuilderAPI_MakeEdge(arc3).Edge()    edge4 = BRepBuilderAPI_MakeEdge(arc4).Edge()      shape =  BRepBuilderAPI_MakeWire(edge1, edge2, edge3, edge4).Wire()        return shape def slide_01_DaoClassic(r):        drawCircle(r, 'stInfo')    pntsBase = getPntsBase(r)    drawPoints(pntsBase, 'stFocus', 'b')    shapeDaoClassic = getWireDaoClassic(pntsBase)    ScShape(shapeDaoClassic, 'stMain')
Рис 01. Контур классического ДаоРис 01. Контур классического Дао

Прикладываю ссылку на WebGL-презентацию: Слайд 01 Контур классического Дао

Здесь вы можете посмотреть этот чертеж в объеме. Если у вас есть 3D-телевизор или 3D-проектор то возможен просмотр в стерео-режиме. Просто нажмите иконку 3D - 1 раз - перекрестный взгляд - 2 раза - режим SideBySide.

В завершение этого этапа хочу немного поразмышлять - это у меня такая беда - размышлять по самым пустяковым поводам.


Если вы задумайте определять координаты точек и расстояния, с помощью теоремы Пифагора и тригонометрических знаний, полученных в 6-ом классе, то это конечно сработает, но настоящие CAD-индейцы так не поступают. Все что вам нужно в плане нахождения координат, углов, расстояний и направлений делается с помощью векторной алгебры без привлечения каких-либо других допотопных методов. Создатели 3D-библиотек очень расстроятся, если вы вдруг для поворотов в пространстве не будете использовать квартернионы, которые они так заботливо реализовали специально, чтобы облегчить вам жизнь.

Не вздумайте задавать углы в градусах и писать функции типа DegreeToRadian. Импортируйте константу pi и задавайте углы поворота только как pi, pi/4, -pi/8 и так далее. Если кто-то из ваших знакомых прознает, что вы все еще мыслите в градусах, о вас поползет дурная слава. В мире математики вы станете изгоем и даже выпускники 9-ых классов никогда не подадут вам руки. Чтобы как-то обосновать эту мысль, скажу, что вся тригонометрия вычисляется на компьютере при помощи рядов и значение в радианах сразу можно подставить в ряд без каких-либо преобразований. В общем, давайте беречь ресурсы наших компьютеров.

На самом деле вопрос не столь очевиден. Так когда мне в 8 классе сказали, что теперь начинается другая жизнь и отныне мы будем измерять углы в естественных единицах - радианах, я очень удивился. Ничего естественного в том, чтобы задавать углы иррациональными числами я не увидел. Наиболее естественным мне представлялась измерять углы а оборотах - 1 - 1 оборот, 2 - 2 оборота. Конечно я тогда был наивен и не знал про ряды, пределы, гармонический анализ , и про то что вся математическая кухня намного упрощается если исходить из радианов.


Здесь уже можно перейти к следующему шагу и попробовать немного улучшить, то что придумали китайцы шесть тысяч лет назад.

Шаг 5. Улучшаем совершенство

Давайте сделаем так, чтобы между нашими Инь и Янь был некоторый отступ. Так мы подготовим базу для объемных построений. Согласитесь, если объемные тела будут соприкасаться это будет не очень красиво.

Здесь мы проведем первое испытание библиотеки на математическую прочность. Используем функцию отступа. Она называется Offset. Для этого нам понадобится еще один параметр - размер отступа.

def getShapeOffset(shape, offset):    tool = BRepOffsetAPI_MakeOffset()    tool.AddWire(shape)    tool.Perform(offset)    shape = tool.Shape()      return shapedef slide_02_DaoConcept(r, offset):        drawCircle(r + offset, 'stInfo')    pntsBase = getPntsBase(r)    wireDaoClassic = getWireDaoClassic(pntsBase)    wireDao0 = getShapeOffset(wireDaoClassic, -offset)    ScShape(wireDao0, 'stMain')      pntsDao0 = getPntsOfShape(wireDao0)    drawPoints(pntsDao0, 'stFocus', 'd')      wireDao1 = getShapeOZRotate(wireDao0, pi)    ScShape(wireDao1, 'stInfo')
Рис 02. Контур Дао с отступомРис 02. Контур Дао с отступом

Ссылка на WebGL-презентацию: Слайд 02 Контур Дао с отступом

Ура, получилось. Обратите внимание, как бережно библиотека обошлась с топологией. Количество точек было сохранено и они оказались именно там где нужно. Я считаю это большое достижение для создателей библиотеки. Настало время для серьезных дел - выходим в 3D.

Шаг 6. Строим сечение. Заметки о самом главном.

Как задать форму объемного тела? Один из методов заключается в том, чтобы задать сечения объекта. Причем если нам удастся сделать это непрерывным образом - считайте дело в шляпе. С точки зрения математики наша задача решена. Кому-то этот шаг может показаться невзрачным, но именно он является САММ ГЛАВНМ с точки зрения построения объекта.

Если мы внимательно посмотрим на наш объект, то с геометрической точки зрения мы отчетливо можем выделить две составляющие - голову круглой формы и хвост, форма которого вообще не имеет определенного названия. Сечения для головы и хвоста должны строится по-разному.

Голову мы будем рассекать параллельными прямыми. Понятно, что в результате получится полусфера, но чтобы сохранить общий подход мы все-таки построим ее с помощью сечений. Сечения же для хвоста будем проецировать из некоего фокуса. Где должен быть этот фокус? Фокус должен находится в точке, откуда все сечения будут максимально условно перпендикулярны к объекту. Путем подбора я определил, что наилучшие результаты получаются когда фокус находится на оси Y на расстоянии -r/4 от центра.

Сечение будем задавать с помощью параметра k. При к = 0 мы находимся в начале нашей фигуры. При k = 1 в конце. Во всех промежуточных значениях алгоритм должен строить сечение объекта. Что касается самого сечения - предположим что это окружность (раз уж здесь везде окружности)

def getPntDaoFocus(r):    return gp_Pnt(0,-r/4,0)def getPntsForDaoSec(pntDaoStart, pntUpLimit, pntDaoEnd, pntDownLimit, pntFocus, k):    angleLimit = 0    pntLimit = getPntScale(pntFocus, pntUpLimit, 1.2)    angleStart = getAngle(pntFocus, pntLimit, pntDaoStart)    angleEnd = getAngle(pntFocus, pntLimit, pntDaoEnd)    kLimit = (angleLimit - angleStart)/(angleEnd - angleStart)    if k < kLimit: #head        kHead = (k - 0) / (kLimit- 0)        xStart = pntUpLimit.X()        xEnd = pntDaoStart.X()        dx = (xEnd-xStart)*(1 - kHead)        pnt0 = getPntTranslate(pntFocus, dx, 0, 0)        pnt1 = getPntTranslate(pntLimit, dx, 0, 0)    else: #tail            kTail = (k - kLimit) / (1 - kLimit)        angle = -angleEnd*kTail        pnt0 = pntFocus        pnt1 = getPntRotate(pntFocus, pntLimit, angle)    return pnt0, pnt1def getWireDaoSec(shapeDao, pntFocus, k):        pntsDao = getPntsOfShape(shapeDao)    pntDownLimit, pntDaoStart, pntUpLimit, pntDaoEnd = pntsDao        p1, p2 = getPntsForDaoSec(pntDaoStart, pntUpLimit, pntDaoEnd, pntDownLimit,                               pntFocus, k)    sectionPlane = getFacePlane(p1, p2, 3)        pnt0, pnt1 =  getPntsEdgesFacesIntersect(shapeDao, sectionPlane)    pntUp = getPntSectionUp(pnt0, pnt1)    circle = GC_MakeCircle(pnt0, pntUp, pnt1).Value()    edge = BRepBuilderAPI_MakeEdge(circle).Edge()    wire =  BRepBuilderAPI_MakeWire(edge).Wire()    return wire   def slide_03_DaoSecPrincipe(r, offset, k, h):        drawCircle(r + offset,  'stInfo')    pntsBase = getPntsBase(r)    wireDaoClassic = getWireDaoClassic(pntsBase)    wireDao0 = getShapeOffset(wireDaoClassic, -offset)    ScShape(wireDao0, 'stMain')        # for oure goal we need divide Dao on Head and Tail    # Head sections is parallell    # Tail sections is focused on focus point    pntsDao0 = getPntsOfShape(wireDao0)    pntDownLimit, pntDaoStart, pntUpLimit, pntDaoEnd  = pntsDao0        # we need focus to determine tail sections     pntFocus = getPntDaoFocus(r)    ScPoint(pntFocus, 'stMain')    ScLabel(pntFocus, 'F' ,'stMain')        # we need two points to determine section    pnt1, pnt2 = getPntsForDaoSec(pntDaoStart, pntUpLimit, pntDaoEnd,                                   pntDownLimit, pntFocus, k)    ScLine(pnt1, pnt2, 'stFocus')        # !!! we need use plane to detect intercsect (not line) becouse 3D    planeSec = getFacePlane(pnt1, pnt2, h)    ScShape(planeSec, 'stFocus')    pntsSec =  getPntsEdgesFacesIntersect(wireDao0, planeSec)    drawPoints(pntsSec, 'stFocus')        wireSec = getWireDaoSec(wireDao0, pntFocus, k)    ScShape(wireSec, 'stFocus') 
Рис 03 Принцип построения сеченийРис 03 Принцип построения сечений

Ссылка на WebGL-презентацию: Слайд 03 Принцип построения сечений

Здесь понадобилась еще одна нетривиальная операция - пересечение кривой и поверхности. В качестве кривой выступает построенный нами двухмерный дао-контур. В качестве поверхности выступает вспомогательная плоскость, она проходит из фокуса под углом, определяемым нашим k. Алгоритм пересечения работает надежно и устойчиво. При первых попытках мне показалось, что он выдает ошибку. Но в результате оказалось что ошибался я, а алгоритм отрабатывает на пятерочку.

Хотелось бы еще немного порассуждать вот на какую тему? Почему мы ищем пересечение кривой и плоскости - не проще ли найти пересечение прямой и кривой? Ответ в том, что не проще. Не забывайте, что мы в 3D а здесь пересечение двух кривых необычное и редкое событие. Данный алгоритм попросту не существует.

Можно конечно вернуться в 2D, но это сложный путь. Из 2D в 3D перейти просто, а обратно гораздо сложнее. Поэтому думаю, что выйдя однажды в 3D лучше оставаться там до конца карьеры :) Хорошая новость заключается в том что практически для любой проблемы существуют изящные 3D решения.


Раз уж была затронута эта тема, хочу немного сказать о 3D-мышлении. Мне очень нравится все что касается объемного конструирования, это затрагивает какие-то базовые нейронные структуры, возможно где-то в глубине, на уровне мозжечка и я от всего этого испытываю реальный кайф. Откуда это у меня - я думаю все дело в близких мне людях.

Хочется рассказать про отца. Это боевой конструктор старой закалки. И я до сих пор учусь у него использовать инженерный подход к различным проблемам, начиная от философских и заканчивая бытовыми.

Еще хочется рассказать о моих институтских друзьях. Когда я учился в институте в нашем небольшом провинциальном городке, мне повезло и я, так сказать, попал в 3D-обойму. Это была группа студентов, которых посылали от кафедры графики по всей России на различные соревнования по начертательной геометрии, черчению, конструированию, и прочим вещам. Замечу, что это были лихие девяностые и перед руководителями кафедры часто стоял прямой выбор - выплатить зарплату, купить принтер, или послать группу студентов на соревнования. К чести кафедры выбор почти всегда делался в пользу соревнований. Так вот в этой 3D-обойме были действительно уникальные личности (похоже, что я там был самый бестолковый).

Расскажу один эпизод (а таких эпизодов был вовсе не один). Мой хороший друг Сергей (а он тоже был в обойме), в смутные времена, когда все сидели без денег устроился на некую сомнительную работу. Работа заключалась в том, что где-то на окраине города в сыром и темном подвале стоял высокоточный современный американский металлообрабатывающий центр (я все это видел своими глазами). И Сергей вытачивал на нем на заказ детали весьма причудливой формы. Никакого специального ПО, кроме карандаша и бумаги у него не было. Все команды он вводил непосредственно с пульта станка. Однажды между делом он выточил некую деталь, основу которой составляла коническая многозаходная резьба весьма сложного профиля. Про это узнали. Среди научно-технической элиты города поползли нехорошие слухи, что где-то на задворках происходит какая-то технологическая чертовщина. Слухи достигли авиационного моторного завода и об этом узнал начальник одного из серьезных цехов. Этот начальник все выяснил и лично спустился в сырой и темный подвал, чтобы увидеть все эти чудеса. Потому что весь его отдел, вооруженный Юниграфиксами, очень часто не мог сделать то что нужно.


Надеюсь, я вас немного поразвлек и можно двигаться дальше по узкой китайской тропинке.

Итак, в результате работы алгоритма пересечения получаем две искомые точки, через которые просто проводим симметрично расположенную окружность. Сечение готово. Чтобы проверить, как это работает построим сечения для k от 0 до 1 c постоянным шагом.

def slide_04_DaoManySec(r, offset, kStart, kEnd, cnt):        drawCircle(r + offset, 'stInfo')    pntsBase = getPntsBase(r)    wireDaoClassic = getWireDaoClassic(pntsBase)    wireDao0 = getShapeOffset(wireDaoClassic, -offset)    ScShape(wireDao0, 'stMain')        pntsDao0 = getPntsOfShape(wireDao0)    pntDownLimit, pntDaoStart, pntUpLimit, pntDaoEnd  = pntsDao0        pntFocus = getPntDaoFocus(r)        for i in range(cnt+1):        k = i/cnt        kkScale = kEnd - kStart        kk = kStart + k* kkScale        p0,p1 = getPntsForDaoSec(pntDaoStart, pntUpLimit, pntDaoEnd,         pntDownLimit, pntFocus, kk)        ScLine(p0, p1, 'stFocus')        wireSec = getWireDaoSec(wireDao0, pntFocus, kk)        ScShape(wireSec, 'stMain') 
Рис 04 Форма Дао из сеченийРис 04 Форма Дао из сечений

Ссылка на WebGL-презентацию: Слайд 04 Форма Дао из сечений

Итак мы проникли в святая святых и выяснили форму бесформенного Дао. Что теперь делать с этим сакральным знанием? Как из всего этого получить нормальное тело?

Предлагаю воспользоваться методом, который называется протягиванием. Он заключается в том, что специальному алгоритму предъявляются последовательные сечения тела и он пытается построить поверхность. Все это относится к широкой области моделирования с ограничениями. Еще можно встретить термины скининг (натягивание кожи), пайпинг (делание трубы).

Должен отметить что главное при протягивании - не протянуть ноги, подбирая различные параметры различных алгоритмов. Посмотрим как поведет себя наше ядро.

Шаг 7. Получаем готовую геометрию

Первая мысль - задать как можно больше сечений и алгоритм разберется, что к чему. К сожалению так не получится. Процесс аппроксимации - сложная штука. И в принципе нужно стремится к тому, чтобы ограничений накладываемых на алгоритм было как можно меньше. Если кто-нибудь рисовал сплайны, то он знает, что наиболее красивые формы получаются, когда мы уменьшаем количество точек а не увеличиваем. Нужно выбрать минимальное количество сечений, которые сформируют максимально точную геометрию.

Чтобы процесс протягивания был конструктивным и приятным я сделал следующие вещи.

  • Создал в Python список, содержащий коэффициенты опорных сечений и вывел эти сечения их на экран.

  • Кроме того я вывел на экран сам контур Дао, который мы нарисовали на втором шаге. Как известно этот контур является Абсолютной Истиной. По отношению к этой истине мы и будем оценивать наши результаты.

Не буду утомлять описанием подбора количества и расположения сечений. Скажу лишь, что это не заняло много времени, потому что была организована хорошая обратная связь - сразу было видно где форма отклоняется от идеала. Представляю конечный результат усилий.

def slide_05_DaoSkinning (r, offset):        drawCircle(r + offset,  'stInfo')    pntsBase = getPntsBase(r)    wireDaoClassic = getWireDaoClassic(pntsBase)    wireDao0 = getShapeOffset(wireDaoClassic, -offset)    ScShape(wireDao0, 'stMain')        pntsDao0 = getPntsOfShape(wireDao0)    pntDownLimit, pntDaoStart, pntUpLimit, pntDaoEnd  = pntsDao0        pntFocus = getPntDaoFocus(r)    drawPoints(pntFocus, 'stMain')      ks = [ 3, 9 , 16, 24, 35, 50, 70, 85]     wiresSec = []     for k in  ks:       wireSec = getWireDaoSec(wireDao0, pntFocus, k/100)       ScShape(wireSec, 'stMain')       wiresSec += [wireSec]            solidDao0 = getShapeSkin(pntDaoStart, wiresSec, pntDaoEnd)    ScShape(solidDao0, 'stFocus')
Рис 05 Протягивание поверхности через сеченияРис 05 Протягивание поверхности через сечения

Ссылка на WebGL-презентацию: Слайд 05 Протягивание поверхности через сечения

Хочу обратить внимание на то, как получились начальная и конечные точки геометрии. Алгоритм понял, что начало должно быть гладким, а кончик острым. Честно говоря, я на это не надеялся и предвидел большие проблемы. Но все как-то получилось само собой. Давайте придадим нашей форме законченность. Во первых для придания динамизма и необычности - слегка приплюснем ее по вертикали.

Хочется немного рассказать с какой неожиданной проблемой я столкнулся. Никак не ожидал забуксовать на таком ровном месте. За различные преобразования в OpenCascade отвечает объект gp_Trsf. До сих пор он вел себя прилично и удовлетворительно. Но когда я с помощью его попытался приплюснуть геометрию у меня ничего не получалось. Началось с того, что я не нашел метода изменения масштаба отдельно по осям. Затем я пытался вручную задавать коэффициенты трансформации. Ничего не получалось. Объект магическим способом сопротивлялся всем попыткам.

Истина оказалась проста. OpenCascade конструкторское ядро и оно не для дешевых эффектов Поэтому базовый объект трансформации не позволяет искажать геометрию. Он защищает объекты от нелогичных изменений. Для тех же кто хочет большего и понимает зачем он это делает существует другой объект gp_GTrsf. Вот с помощью его то у меня все и получилось.

Далее мы скопируем нашу форму с разворотом на 180 градусов... Ой, простите на pi ... Да, да... конечно наpi :) И раскрасим все это в приятные цвета.

def getSolidDao(r, offset):        pntsBase = getPntsBase(r)    wireDaoClassic = getWireDaoClassic(pntsBase)    wireDao = getShapeOffset(wireDaoClassic, -offset)        pntsDao = getPntsOfShape(wireDao)    pntDownLimit, pntDaoStart, pntUpLimit, pntDaoEnd  = pntsDao        pntFocus = getPntDaoFocus(r)       ks = [ 3, 9 , 16, 24, 35, 50, 70, 85]     wiresSec = []     for k in  ks:       wireSec = getWireDaoSec(wireDao, pntFocus, k/100)       wiresSec += [wireSec]            solidDao = getShapeSkin(pntDaoStart, wiresSec, pntDaoEnd)    solidDao = getShapeZScale(solidDao, 0.7)    return solidDao   def slide_06_DaoComplete (r, offset):        solidDao0 = getSolidDao(r, offset)    ScShape(solidDao0, stDao0)    solidDao1  = getShapeOZRotate(solidDao0, pi)    ScShape(solidDao1, stDao1)
Рис 06 Окончательная форма Дао Рис 06 Окончательная форма Дао

Ссылка на WebGL-презентацию: Слайд 06 Окончательная форма Дао

Шаг 8. Современная основа для древней философии

Согласитесь, даже гениальные идеи не могут висеть в воздухе, поэтому сделаем небольшую подставку для нашего объемного Дао. А заодно и проверим наше геометрическое ядро на прочность в плане булевых операций с объектами.

Вычесть куб из сферы - и так нелегкая задача. А когда в булевых операциях участвует геометрия, сформированная нетривиальным образом - результат не может предсказать никто. Вот и посмотрим на что действительно способен Open Source.

Код следующего шага несложный, чего не скажешь о стоящей за ним математике. Замечу, что для того, чтобы наши Инь и Янь удобно лежали на подставке я ввел параметр gap - технологический зазор по всему контуру - где-то 1 мм.

def getDaoCase(r, offset, h):    r2 = r*2                                        h2 = h/2    rTop = r + offset    rSphere = gp_Vec(0,rTop,h2).Magnitude()    sphere = BRepPrimAPI_MakeSphere(rSphere).Shape()    limit = BRepPrimAPI_MakeBox( gp_Pnt(-r2, -r2, -h2), gp_Pnt(r2, r2, h2) ).Shape()    case = BRepAlgoAPI_Common(sphere, limit).Shape()    case = getShapeTranslate(case, 0,0,-h2)         solidDao0 = getSolidDao(r, offset)    solidDao1  = getShapeOZRotate(solidDao0, pi)       case = BRepAlgoAPI_Cut(case, solidDao0).Shape()    case = BRepAlgoAPI_Cut(case, solidDao1).Shape()      return case    def slide_07_DaoWithCase (r, offset, caseH, caseZMove ,gap):        solidDao0 = getSolidDao(r, offset+gap)    ScShape(solidDao0, stDao0)    solidDao1  = getShapeOZRotate(solidDao0, pi)    ScShape(solidDao1, stDao1)        case = getDaoCase(r, offset, caseH)        case = getShapeTranslate(case, 0,0, caseZMove)    ScShape(case, stCase)
Рис 07. Форма Дао с основаниемРис 07. Форма Дао с основанием

Ссылка на WebGL-презентацию: Слайд 07 Форма Дао с основанием

Должен сказать, что получилось не сразу, но все-таки получилось. Да, обидно, что ядро спотыкается и мы вынуждены прибегать к шаманизму для получения результата. Но учитывая, какая сложная математика здесь присутствует можно сделать на это поправку.

Невидимое солнце Open Source

Вот и закончилось это увлекательное 3D-мистичекое-приключение. Боги к нам были благосклонны и практически все получилось. Оставляю несколько ссылок:

  • GitHub - Точка сборки - ссылка на репозиторий с проектом, в рамках которого было проделано это исследование.

  • makeDaoShape.py - ссылка на полный текст примера

  • Инь, Янь, Подставка. - ссылки на STL-файлы (мало ли кому пригодятся). Только пожалуйста - не перепутайте Инь и Янь - понятно что отличия минимальны, но кто знает этот загадочный Китай :)

Если подвести итог в целом, то мне понравилось знакомство с OpenCascade. Это просто чудо, что такие вещи лежат в открытом доступе и их можно изучать со всех сторон. Пусть порой было не все гладко. Да и код, который я написал совсем не совершенен. Но ведь идеального не бывает ничего. И это наверное самое интересное свойство нашего мира.

Нам надо знать, когда остановиться
Сосуд нельзя сверх меры наполнять
Большие ценности труднее охранять
И слишком острый меч быстрее притупится
Добром ли чином через чур гордится
Беду на дом свой накликать
Твой труд закончен, так умей уйти
Вот смысл силы, жизни и пути.

Великая книга Дао - Стих 9 ( Перевод Ю. Полежаевой)

Подробнее..

Перевод Рендеринг каустики воды в реальном времени

30.09.2020 10:08:50 | Автор: admin
В этой статье я представлю свою попытку обобщения вычислений каустики в реальном времени с помощью WebGL и ThreeJS. Тот факт, что это попытка, важен, ведь найти решение, работающее во всех случаях и обеспечивающее 60fps сложная, если не невозможная задача. Но вы увидите, что при помощи моей методики можно достичь достаточно приличных результатов.

Что такое каустика?


Каустика это световые узоры, возникающие, когда свет преломляется и отражается от поверхности, в нашем случае на границе воды и воздуха.

Из-за того, что отражение и преломление происходят на волнах воды, вода действует здесь как динамическая линза, создающая эти световые узоры.


В этом посте мы сосредоточимся на каустике, вызванной преломлением света, то есть на том, что обычно происходит под водой.

Чтобы добиться стабильных 60fps, нам нужно вычислять её на графической карте (GPU), поэтому мы будем вычислять каустику только шейдерами, написанными на GLSL.

Для её вычисления нам потребуется:

  • вычислить преломлённые на поверхности воды лучи (в GLSL это легко, потому что для этого существует встроенная функция)
  • вычислить при помощи алгоритма нахождения пересечений точки, в которых эти лучи сталкиваются с окружением
  • вычислить яркость каустики, проверяя точки сближения лучей



Хорошо известное демо воды на WebGL


Меня всегда поражало это демо Эвана Уоллеса, демонстрирующее визуально реалистичную каустику воды на WebGL: madebyevan.com/webgl-water


Рекомендую прочитать его статью на Medium, в которой объясняется, как вычислять каустику в реальном времени при помощи меша фронта света и функций частной производной GLSL. Его реализация чрезвычайно быстра и очень красиво выглядит, но имеет некоторые недостатки: она работает только с кубическим бассейном и сферическим мячом в бассейне. Если поместить под воду акулу, то демо не будет работать: в шейдерах жёстко прописано, что под водой находится сферический мяч.

Он поместил под воду сферу потому, что вычисление пересечения между преломлённым лучом света и сферой это лёгкая задача, в которой используется очень простая математика.

Всё это подходит для демо, но я хотел создать более общее решение для вычисления каустики, чтобы в бассейне могли находиться любые неструктурированные меши, например акула.


Теперь давайте перейдём к моей методике. В этой статье я буду считать, что вы уже знаете основы 3D-рендеринга с помощью растеризации, а также знакомы с тем, как вершинный шейдер и фрагментный шейдер совместно работают над отрисовкой примитивов (треугольников) на экране.

Работа с ограничениями GLSL


В написанных на GLSL (OpenGL Shading Language) шейдерах мы можем иметь доступ только к ограниченному объёму информации о сцене, например:

  • Атрибуты текущей отрисовываемой вершины (позиция: 3D-вектор, нормаль: 3D-вектор и т.п.). Мы можем передавать свои атрибуты GPU, но они должны иметь встроенный тип GLSL.
  • Uniform, то есть константы для всего текущего отрисовываемого меша в текущем кадре. Это могут быть текстуры, матрица проецирования камеры, направление освещения и т.п. Они должны иметь встроенный тип: int, float, sampler2D для текстур, vec2, vec3, vec4, mat3, mat4.

Однако нет возможности получать доступ к мешам, присутствующим в сцене.

Именно поэтому демо webgl-water можно сделать только с простой 3D-сценой. Проще вычислять пересечение преломлённого луча и очень простой фигуры, которую можно представить с помощью uniform. В случае сферы её можно задать позицией (3D-вектор) и радиусом (float), поэтому эту информацию можно передавать шейдерам с помощью uniform, а для вычисления пересечений требуется очень простая математика, легко и быстро выполняемая в шейдере.

Некоторые выполняемые в шейдерах методики трассировки лучей передают меши в текстурах, но в 2020 году при рендеринге реального времени на WebGL такое решение неприменимо. Нужно помнить, что для получения достойного результата мы должны вычислять 60 изображений в секунду с большим количеством лучей. Если мы вычисляем каустику, используя 256x256=65536 лучей, то каждую секунду нам придётся выполнять значительное количество вычислений пересечений (которое также зависит от количества мешей в сцене).

Нам нужно найти способ представить подводное окружение в виде uniform и вычислить пересечение, сохраняя при этом достаточную скорость.

Создание карты окружений


Когда требуется вычисление динамических теней, то хорошо известной техникой является shadow mapping. Она часто используется в видеоиграх, хорошо выглядит и быстро выполняется.

Shadow mapping это техника, выполняемая в два прохода:

  • Сначала 3D-сцена рендерится с точки зрения источника освещения. Эта текстура содержит не цвета фрагментов, а глубину фрагментов (расстояние между источником освещения и фрагментом). Эта текстура называется картой теней (shadow map).
  • Затем карта теней используется при рендеринге 3D-сцены. При отрисовке фрагмента на экране мы знаем, есть ли другой фрагмент между источником освещения и текущим фрагментом. Если это так, то мы знаем, что текущей фрагмент находится в тени, и нужно отрисовывать его чуть темнее.

Подробнее о shadow mapping можно прочитать в этом превосходном туториале по OpenGL: www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping.

Также можно посмотреть интерактивный пример на ThreeJS (нажмите T, чтобы отобразить в левом нижнем углу карту теней): threejs.org/examples/?q=shadowm#webgl_shadowmap.

В большинстве случаев эта методика работает хорошо. Она может работать с любыми неструктурированными мешами в сцене.

Сначала я подумал, что могу использовать похожий подход для каустики воды, то есть сначала отрендерить подводное окружение в текстуру, а затем использовать эту текстуру для вычисления пересечения между лучами и окружением. Вместо рендеринга только глубины фрагментов я также рендерю позицию фрагментов на карте окружения.

Вот результат создания карты окружения:


Env map: в каналах RGB хранится позиция по XYZ, в альфа-канале глубина

Как вычислить пересечение луча и окружения


Теперь, когда у меня есть карта подводного окружения, нужно вычислить пересечение между преломлёнными лучами и окружением.

Алгоритм работает следующим образом:

  • Этап 1: начинаем с точки пересечения между лучом света и поверхностью воды
  • Этап 2: вычисляем преломление с помощью функции refract
  • Этап 3: переходим от текущей позиции в направлении преломлённого луча по одному пикселю в текстуре карты окружения.
  • Этап 4: сравниваем зарегистрированную глубину окружения (хранящуюся в текущем пикселе текстуры окружения) с текущей глубиной. Если глубина окружения больше, чем текущая глубина, то нам нужно двигаться дальше, поэтому мы снова применяем этап 3. Если глубина окружения меньше текущей глубины, то это значит, что луч столкнулся с окружением в позиции, считанной из текстуры окружения и мы нашли пересечение с окружением.



Текущая глубина меньше, чем глубина окружения: нужно двигаться дальше


Текущая глубина больше, чем глубина окружения: мы нашли пересечение

Текстура каустики


После нахождения пересечения мы можем вычислить яркость каустики (и текстуру яркости каустики) с помощью методики, описанной Эваном Уоллесом в его статье. Получившаяся текстура выглядит примерно так:


Текстура яркости каустики (обратите внимание, что эффект каустики менее важен на акуле, потому что она ближе к поверхности воды, что снижает сходимость лучей освещения)

Эта текстура содержит информацию о яркости освещения для каждой точки в 3D-пространстве. При рендеринге готовой сцены мы можем считывать эту яркость освещения из текстуры каустики и получить следующий результат:



Реализацию этой методики можно найти в репозитории Github: github.com/martinRenou/threejs-caustics. Поставьте ей звёздочку, если вам понравилось!

Если вы хотите посмотреть на результаты вычисления каустики, то можете запустить демо: martinrenou.github.io/threejs-caustics.

Об этом алгоритме пересечения


Это решение сильно зависит от разрешения текстуры окружения. Чем больше текстура, тем лучше точность алгоритма, но тем и дольше выполняется поиск решения (прежде чем найти его, нужно считать и сравнить большее количество пикселей).

Кроме того, считывание текстуры в шейдерах приемлемо, если вы не делаете это слишком много раз; здесь мы создаём цикл, продолжающий считывать из текстуры новые пиксели, что не рекомендуется.

Более того, в WebGL запрещены циклы while (и на то есть веская причина), поэтому нам нужно реализовать алгоритм в цикле for, который может быть развёрнут компилятором. Это означает, что нам требуется условие завершения цикла, известное во время компиляции, обычно это значение максимальной итерации, которое заставляет нас прекратить поиск решения, если мы не нашли его в течение максимального количества попыток. Такое ограничение приводит к неверным результатам каустики, если преломление оказывается слишком важным.

Наша методика не так быстра, как упрощённый способ, предолженный Эваном Уоллесом, однако она гораздо более гибкая, чем подход с полномасштабной трассировкой лучей, а также может использоваться для рендеринга в реальном времени. Однако скорость по-прежнему зависит от некоторых условий направления света, яркости преломлений и разрешения текстуры окружения.

Завершаем обзор демо


В этой статье мы рассмотрели вычисление каустики воды, но в демо использовались и другие техники.

При рендеринге поверхности воды для получения отражений мы использовали текстуру скайбокса и кубические карты. Также мы применили преломление к поверхности воды с помощью простого преломления в экранном пространстве (см. эту статью об отражениях и преломлениях в экранном пространстве), эта техника физически некорректна, однако визуально убедительна и быстра. Кроме того, для большего реализма мы добавили хроматические аберрации.

У нас есть ещё идеи по дальнейшему улучшению методики, в том числе:

  • Хроматические аберрации на каустике: сейчас мы применяем хроматические аберрации к поверхности воды, но этот эффект также должен быть видим на подводной каустике.
  • Рассеивание света в объёме воды.
  • Как посоветовали Мартин Жерар и Алан Волф в Twitter, мы можем повысить производительность с помощью иерархических карт окружения (которые будут использоваться как деревья квадрантов для поиска пересечений). Также они посоветовали рендерить карты окружения с точки зрения преломлённых лучей (предполагая, что они совершенно плоские), благодаря чему производительность станет независимой от угла падения освещения.

Благодарности


Эта работа по реалистичной визуализации воды в реальном времени была проведена в QuantStack и финансировалась ERDC.
Подробнее..

Оптимизация 3D-графики под WebGL (опыт PLANT-SIM)

09.10.2020 12:10:51 | Автор: admin

В этой статье речь пойдет об оптимизации Unity-сцены проекта Plantsim 1.0.: о визуальной части цифровой копии предприятия Tennessee Eastman Process, реализованного на Unity 2017.1.1f1.


image


Заметка от партнера IT-центра МАИ и организатора магистерской программы VR/AR & AI компании PHYGITALISM.


В первой версии работа осуществлялась с использованием мощного компьютера, а так же стороннего вычислительного устройства PLC. Отличие новой версии заключается в том, что теперь это WEB-приложение, реализованное с помощью WebGL, и для его работы потребуется только ноутбук.


Платформа Unity Команда Зачем
PlantSim 1.0 PC на базе: Intel i7 Nvidia GTX 1070ti 2017.3.0f3 Default Pipeline Несколько 3D художников Энтузиазм моделить и творить красоту
PlantSim 2.0 Ноутбук: Intel i5 Nvidia 1050ti Google Chrome 2019.2.15f1 LWPR Один 3D художник Ещё больше энтузиазма победить страшную задачу

Таблица сравнения разработки проектов


В сравнении с приложением PlantSim 1.0, заметно снижение требований к характеристикам компьютера. Поэтому оптимизация стала главной задачей для PlantSim 2.0.


image
Пайплайн разработки PlantSim 2.0


Анализ предстоящей работы


WebGL это работа 3D графики с использованием возможностей браузеров Google Chrome, Mozilla, Safari. Первостепенными задачами для нас являлись оптимизация, сохранение того же уровня реализма и поддержание работоспособности приложения на 30FPS+. Для этого нам предстояло работать в пайплайне мобильной разработки.


image
Unity: WebGL Build


Красивой визуализации в WebGL добиться можно, но сложно. Имеется ряд особенностей, о которых желательно знать, если вы собираетесь сделать подобный проект самостоятельно и/или работаете с 3D графикой впервые.


Перед тем, как начать непосредственно работу над 3D, мы выстроили четкий план со списком моделей, которые необходимо оптимизировать.


image
Таблица с планом оптимизации для отслеживания хода работы


Вот краткий список моделей, что присутствовали в сцене раньше:


  • OutPutTank 2 шт
  • Reactor 1 шт, модель была раздельная
  • ReactorExplosion 1 шт, отдельная анимации разлета уничтоженного реактора
  • Condenser 1 шт
  • Separator 1 шт
  • PipesSystem различная система труб на сцене
  • Stripper 1 шт
  • Valve 6 шт
  • Tank 4 шт
  • Refrigerator 1 шт
  • Compressor 3 шт
    В PlantSim 1.0 на сцене было 189 736 трисов, и главной задачей для нас была оптимизация модели на 60%. Таким образом мы получим 75 895 треугольников, что будет удовлетворять нашему полигональному бюджету.
    image
    График расчета суммарного предполагаемого количества полигонов после оптимизации

Оптимизация меша


Оценивая старые модели, я заметил, что некоторые объекты были разделены и представляли собой уникальный меш, с собственными материалами, а иногда даже несколькими. Например:


image
Модель Reactor (PlantSim 1.0) анализ иерархии и сетки


  • Нелогичные наименования объектов
  • Много отдельных элементов
  • Сложная топология

Сам Reactor имел 21998 треугольников. По плану нам было необходимо избавиться от всего лишнего и малозначительного, и получить 8800 треугольников. Перед тем, как начать оптимизировать, мы проанализировали объекты вновь и заметили новую важную деталь: модели OutPutTank, Reactor и Striper имели одни и те же элементы основание и лестницу. Это означало, что эти элементы можно дублировать и использовать для них один материал.


В итоге, после работы над Reactor объектом (это первый объект, с которого началась оптимизация), мы получили новую модель в 7258 треугольников. Напомню, что по плану было 8000. При условии, что теперь UV карта одна, объект представляет собой объединенный меш и один материал стало ясно, что задачу мы сможем реализовать, а итоговая сумма треугольников окажется намного меньше, чем планировали изначально.


image
Модель Reactor (PlantSim 2.0) оптимизированная иерархия и сетка


  • Исправили наименования объектов
  • Уменьшили количество отдельных элементов
  • Упростили и оптимизировали топологию

Объект ReactorExplosion имел сложную анимацию взрыва, красивую и эффектную разлет маленьких кусков реактора с сохранением только его основания. После первой полной сборки сцены наш FPS сильно проседал непосредственно на моменте взрыва, который помимо этой анимации меша так же имел и анимацию, состоящую из 1000 частиц инструмента Particle System. Появился вопрос, что создавало трудности для отрисовки: анимация или частицы? Мы обсудили задачу с Unity разработчиком и выявили, что оба фактора слишком тяжелые для WebGL движка, поэтому было решено оптимизировать эффект взрыва, используя 5 частиц (это возможно благодаря FlipBook текстурам). Со стороны разработки так же была необходима оптимизация анимации внутри Unity.


FlipBook это покадровая анимация, вложенная в одну текстуру. Соответственно, чем больше кадров, тем больше разрешение самой текстуры и ее вес. Этот способ отрисовки анимации наиболее оптимальный, так как вместо огромного количества частиц и сложных симуляций мы обходимся одной или несколькими частицами, в которых за один цикл проигрывается наш взрыв. Таким образом, используя всего 5 частиц, был оптимизирован эффект с исходными 1000.


С анимацией было сложнее, так как каждый осколок Reactor был индивидуальным и имел свою запеченную анимацию это и тянуло WebGL вниз по FPS. Со стороны Unity разработки было предложено запечь всю анимацию в один объект, а метод отрисовки с Mesh renderer переключить на Skinned mesh renderer. Таким образом вместо одновременного перемещения тысячи transform points у нас была цепочка костей и один transform points (Origin самого объекта). Как итог, FPS вырос с 35 во время воспроизведения эффекта до 1520 во время запуска анимации, благо этот фриз сохранялся лишь 0,5 секунды. Кстати, ReactorExplosion объект имел 41220 треугольников, и в угоду экономии времени, которого было мало, было решено не создавать новую анимацию с нуля, а использовать старую, оптимизируя описанным способом.


В итоге работа по оптимизации меша закончилась успешно. Выше мы упоминали, что на сцене располагалось 189736 треугольников (если не учитывать ReactorExplosion, то 148516 треугольников). Отнимая 60% всей геометрии, мы хотим получить на выходе 59403 треугольника (после оптимизации). У нас получилось 61064 треугольника, что превысило наши планы по оптимизации, но все равно было намного ниже полигонального бюджета, выставленного в самом начале проекта. С учетом ReactorExplosion было 102284 треугольника, что так же было около границы обозначенного бюджета.


Задача выполнена, переходим к текстурированию.


image
График расчета суммарного фактического количества полигонов после оптимизации


Текстурирование


Текстурирование играет важную роль в визуализации моделей. Если вы никогда не слышали о том, из чего состоит модель в ее привычном виде, то давайте вспомним:


  • Base Color/Albedo RGBA изображение, определяющее, какой цвет у поверхностей. Иногда идет с Alpha каналом прозрачности поверхностей.
  • Ambient Occlusion Ч/Б изображение с информацией затенений объекта.
  • Metallness Ч/Б изображение, определяющее металлическую природу поверхности.
  • Smoothness Ч/Б изображение, определяющее степень размытости поверхности или ее мелких отдельных деталей.
  • Normal Map RGB изображение, симулирующее светотень от мелких неровностей поверхности.
  • Height Map Ч/Б изображение, отвечающее за степень искажения неровностей поверхности. Часто используется в связке с Normal Map.
  • Emission RGB изображение, отвечающее за самосвечение отдельных участков или всей поверхности объекта. Может влиять на Global Illumination параметры, то есть участвовать в освещении других объектов в сцене.
    image
    Набор текстур для модели Striper из проекта PlantSim 2.0

Благодаря текстурным картам имитируются поверхности на моделях. После анализа списка можно заметить сходства помимо RGB текстур мы имеем много черно-белых карт. Что же это значит для оптимизации?


Дело в том, что отдельно R G и B, а также A каналы и представляют собой черно-белый слой. А значит отдельно в одну текстуру RGBа можно зашифровать сразу четыре текстуры. Как это работает:


image
Комбинирование четырех ч/б текстурных карт в одну текстуру


Далее эту Combine texture расшифровывают и отдают в отдельные каналы материала в Unity.
Также хотелось бы упомянуть Normal Map. Эта текстура мощный инструмент для оптимизации модели, с помощью нее возможно избавиться от мелкой детализации модели и впоследствии отобразить при рендеринге на сцене.


image
Сравнение двух моделей Compressor


На этом примере наглядно видна разница поверхностей старой модели и новой, оптимизированной. Данную текстуру создают двумя способами:


  1. Отрисовка вручную в специальных программах, например Substance Painter, используя подготовленные кисти неровностей.
  2. Запекание неровностей с HighPoly модели (с повышенной детализацией, фасками, неровностями и углублениями) на LowPoly модель, которая максимально оптимизирована. Данную процедуру используют как в Substance Painter, так и в Blender инструментах.

Материалы и шейдеры


Использование одного и того же контента повторно это экономия места на UV развертках, которая позволяет уделить больше внимания деталям в освободившемся пространстве. В нашем случае это были элементы основания реакторов и лестницы.


image
Набор материалов на различных объектах. Один цвет один материал


На примере с помощью цвета отображено, какие материалы используются на том или ином объекте. Можно заметить, что один и тот же материал используются на разных объектах, то есть они имеют одну UV развертку. Это важный момент, так как если объект маленький и незначительный, значит сама UV развертка будет маленькой и менее детальной для объекта.


Объединение подобных объектов с другими позволяет экономить на количестве суммарно используемых текстурных карт.


WebGL ограничен в отрисовке графики, а также в реализации функционала. Но если учитывать эти особенности в разработке, то можно найти варианты замены или обхода ограничений. Главное проведение исследования каждой возникающей проблемы.


Запекание света


Давайте поговорим о том, как работает свет в Unity, а именно о типе освещения Global Illumination. Это честный способ отображения отражения света от поверхности объекта и создания теней. В Unity тени строятся по двум типам объектов Dynamic и Static объекты.


В случае динамических объектов тени просчитываются вновь и вновь, вместе с каждой анимацией и движением.


В случае статичных объектов подразумевается, что они находятся в покое, а тени всегда сохраняют свое положение. Как раз для таких объектов можно запечь тень объекта на пространство в специальный Lightmap текстуры. Учитывая задачу оптимизации и отказ от Realtime просчета теней, это наиболее оптимальный подход.
image
Представление сцены без Lightmap текстурной карты и после ее создания
В PlantSim 2.0 мы запекали тени от всех объектов, если они не участвовали в анимациях.


Хитрости в работе с WebGL


К финалу разработки мы стали сражаться за количество FPS уже в собранных билдах, отслеживая напрямую в Google Chrome. Сохранялась проблема почему FPS на ноутбуке c i7 процессором и GTX 1070ti не повышается больше 30, а наоборот, иногда даже проседает. Ведь не может быть так, что всей проделанной работы было недостаточно.


Перед тем, как запускать проект на референсном ноутбуке заказчика, мы проводили тесты на различных устройствах (ноутбуки, персональные компьютеры), и анализировали FPS, которое показывало приложение.


image
Таблица стресс-тестов на различных устройствах. Скриншоты фиксируют одинаковую ситуацию и количество FPS в данный момент.


В тестировании участвовали как мощные ноутбуки на базе Windows и Apple MacBook PRO, так и слабые ноутбуки, на которых не предполагалась работа с 3D графикой. Для чистоты тестирования мы так же использовали стационарный компьютер с RTX 2080ti (кстати, на нем FPS был стабильно 60). FPS оказался разным у всех устройств.


Разгадка оказалась куда проще, чем предполагалось изначально. Дело в том, что работу браузера Google Chrome определяет некоторые факторы:


  1. Подключен ли ноутбук к зарядке? Если нет, то вероятно, что мощность видеокарты значительно снижена из за экономии аккумулятора.
  2. Есть ли на ноутбуке интегрированная видеокарта (видеокарта, которая встроена в центральный процессор или материнскую плату)?

Проверить первый пункт достаточно легко, второй уже сложнее. Мы попробовали дать конкретную задачу для приложения Google Chrome, сделать запуск с параметрами повышенной мощности, однако результатов это не давало. Видеокарты фирмы NVIDIA имеют отдельное приложение по управлению ресурсами видеокарты на устройстве NVIDIA control panel. В этом приложении мы нашли Google Chrome и вручную выставили работу с помощью дискретной видеокарты, а не интегрированной.


image
Окно Nvidia Control Panel настройки производительности для отдельного приложения


В итоге, после очередного тестирования билда на Google Chrome, счетчик FPS победно показывал 7090 это означало, что во всех предыдущих тестированиях в отрисовке 3D графики браузера участвовала куда более слабая видеокарта (интегрированная), из-за стараний ноутбука сэкономить заряд батареи.


Вывод


Разработка проекта такого рода специфична, но во многом интересна, так как WebGL это новое направление работы с 3D графикой и представление ее в WEB.


Все описанные методы выше не новые, но важные и влияют на скорость загрузки приложения, и насколько будет комфортно пользователю просматривать в браузере.


Полезные ссылки:


WebGL Wikipedia
Unity Wikipedia
Unity Официальный сайт
Unity WebGL Builds
Unity WebGl Development
UV развертки Wikipedia
Texturing mapping Wikipedia
PBR материалы Habr
3D modeling Wikipedia
PlantSim 1.0
PlantSim 2.0

Подробнее..

Почему мы трансформируем трёхмерные векторы матрицами 4х4?

03.02.2021 08:16:01 | Автор: admin

Почему не матрица 3х3? Почему в матрице 4х4 всё уложено именно так? Зачем там последняя строка, заполненная нулями и одной единицей в конце? Этими вопросами я задался накануне, решил поисследовать вопрос и рассказываю что выяснил.

В статье нас будут интересовать только афинные преобразования, а в частности вращение, масштабирование и перемещение, которые активно используются в программировании графики и разработке игр в целом.

Афинное преобразование является композицией двух функций: линейной и нелинейной трансформаций. Через линейные реализуются вращение и масштабирование, а сама трансформация задается матрицей той же размерности, что и пространство, в котором она применяется (Ax). Через нелинейные реализуются перемещения, но из свойства таковы, что такие трансформации не могут быть выражены матрицей, зато легко могут быть выражены слагаемым вектором (+b).

Афинное преобразование T, примененное на вектор x можно записать как

T(\vec{x}) = A\vec{x}+\vec{b}

Трансформации одномерного вектора

Благорадя одномерности для простоты мы можем представить одномерную матрицу А, вектор b и вектор x как числа на вещественной прямой. Так же трансформированное значение x предпочту записывать как x', мне кажется так выглядит чуть более чисто:

\begin{array}{ll} A\rightarrow [a] \rightarrow a, \\ \vec{b} \rightarrow (b) \rightarrow b, \\ \vec{x} \rightarrow x, \\ T(x) \rightarrow x' \end{array}

Итого

x' = ax+b

Через манипуляции с а мы можем растягивать или сжимать вектор x (линейно трансформировать), а через манипуляции с b перемещать (нелинейно трансформировать).

Случаи, когда используются обе, линейная и нелинейная трансформации вместе довольно частые. Было бы удобно найти такое одно преобразование M, чтобы выразить в нём сразу оба:

x' = ax+b = M(x)

Возьмём для примера трансформацию x' = 3x + 4 (3x линейная трансформация и +4 нелинейная трансформация) и попробуем подобрать нужную матрицу.

\begin{array}{ll} Mx = 3x + 4\\ M = (3x+4)/x\\M=3+(\frac{4}{x})\end{array}

Свойства линейных трансформаций таковы, что они могут быть выражены через матрицы (например преобразование 3x может быть выражено через одномерную матрицу [3]), однако нелинейные трансформации (x+4) таких свойств лишены, от чего не удается выразить M без зависимости от x.

Трюк: поднимается на размерность выше

Если представить +4 как +4y, введя дополнительную координату y, выразив её так же через x и саму себя, то получится система линейных уравнений

x' = 3x + 4y \\ y' = \_x + \_y

которую можно выразить через матрицу 2x2, которая в свою очередь может выразить x' = 3x+4 и при этом не будет зависеть от x, т.е будет линейной трансформацией. Нижнюю строку я не заполнил конкретными числами, потому что на данный момент они не важны.

\begin{bmatrix} x'\\ y' \end{bmatrix} = \begin{bmatrix} 3 & 4 \\ \_ & \_ \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}

Так как матрица 2x2 умножается только на двумерный вектор, то необходимо предоставить не только х, но и вторую координату - y, а так как она учавствует в выражении +4y и мы хотим, чтобы это превратилось просто в +4, то вместе с неизвестным x на умножение с матрицей передаем единицу:

\begin{bmatrix} x'\\y' \end{bmatrix}= \begin{bmatrix} 3 & 4\\ \_ & \_ \end{bmatrix} \begin{bmatrix} x \\ 1 \end{bmatrix} \rightarrow \begin{array}{l} x' = 3\cdot x + 4\cdot 1\\y' = \_\cdot x + \_\cdot 1 \end{array}

Второе выражение в системе нам не интересно, оно введено только для того, чтобы иметь возможность получить матрицу, однако в результате вычислений будет возвращен двумерный вектор, с 3x+4 для x' и чем-то для y' и было бы сподручнее получить единицу в y' вместо непонятно чего, ведь в таком случае мы получим удобный вектор

\begin{bmatrix} x'\\1 \end{bmatrix}

который будет удобно умножать на любую другую матрицу далее. Чтобы получить единицу заполняем выражение соответствующими коэффициентами: y' = 1 = 0 x + 1 1

\begin{bmatrix} x'\\1 \end{bmatrix}= \begin{bmatrix} 3 & 4\\ 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ 1 \end{bmatrix}

Вот так у нас получается матрица, которая может быть выражена независимо от x и способна выполнять афинные трансформации над одномерными векторами, заключенными в двумерные.

\begin{bmatrix} a & b\\0 & 1 \end{bmatrix}

Получается в матрице заключена линейная трансформация (a), нелинейная трансформация (b) и служебная строка (0 1) которая сохраняет для y' значение 1, чтобы вычисления x' проходили так, как мы ожидаем.

На самом деле трансформация - это просто хитрое слово, обозначающее функцию, но предполагаящая отображение некоторого движения. Вот как трансформация из примера выглядит визуально (https://www.geogebra.org/calculator/jvesjzyw):

\begin{bmatrix} 3 & 4 \\ 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 1 \end{bmatrix} = \begin{bmatrix} 7 \\ 1 \end{bmatrix}

Тот же трюк, но в двумерном пространстве

Имеем матрицу для вращения или масштабирования и вектор для перемещения

Чтобы иметь возможность задать матрицей оба преобразования поднимается на размерность выше и выпоняет принципиально те же действия, что в одномерном случае, только теперь новая компонента - это z, потому что y уже существует и нужна.

Матрица линейной трансформации выросла до 2х2 по понятным причинам двумерности пространства. Вектор b тоже вырос до двумерного и способен перемещать по обеим осям.

Вычисляемые значения для x' и y' будут такими же, как если бы мы считали по отдельности каждую трансформацию, а z' всегда будет равен 1 для удобства.

В трёхмерном пространстве ничего нового

Два преобразования, одно (линейное) выражено через матрицу, а другое (нелинейное) через вектор:

Два преобразования выраженных через одну матрицу более высокой размерности:

Полезные материалы

Computing 2D affine transformations using only matrix multiplication

https://medium.com/hipster-color-science/computing-2d-affine-transformations-using-only-matrix-multiplication-2ccb31b52181

Brilliant. Linear Transformations

https://brilliant.org/wiki/linear-transformations/

Explaining Homogeneous Coordinates & Projective Geometry

https://www.tomdalling.com/blog/modern-opengl/explaining-homogenous-coordinates-and-projective-geometry/

Nonlinear Transformation

https://stattrek.com/statistics/dictionary.aspx?definition=Nonlinear-Transformation

Can non-linear transformations be represented as Transformation Matrices?

https://math.stackexchange.com/a/455

Linear transformations and matrices | Essence of linear algebra, chapter 3

https://youtu.be/kYB8IZa5AuE

Подробнее..

Формула Даламбера в действии

04.03.2021 10:11:49 | Автор: admin

Добрый день!

Сегодня я бы хотел Вам представить небольшую программку, которая использует формулу Даламбера:

На мой взгляд, здесь происходит смещение функции одной относительно другой, за счет чего образуется волна(wave). Аргументы функции здесь x и t, можно еще использовать и ускорение a. Две функции fi и ksi, точнее y(вместо ksi).

Программа написана с использованием браузера и WebGL.

Файл Wave.html:

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" /><title>Draw a Wave</title></head><body onload="main()"><canvas id="webgl" width="1600" height="1000">Please use a browser that supports "canvas"</canvas><script src="webgl-utils.js"></script><script src="webgl-debug.js"></script><script src="cuon-utils.js"></script><script src="Wave.js"></script></body></html>

Вам необходимо положить webgl-utils.js webgl-debug.js cuon-utils.js к файлу Wave.html либо найти их в сети и подключить.

Файл Wave.js:

// Vertex shader programvar VSHADER_SOURCE =  'attribute vec4 a_Position;\n' +  'void main() {\n' +  '  gl_Position = a_Position;\n' +  '}\n';// Fragment shader programvar FSHADER_SOURCE =  'void main() {\n' +  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +  '}\n';var xx=0;function main() {  // Retrieve <canvas> element  var canvas = document.getElementById('webgl');  // Get the rendering context for WebGL  var gl = getWebGLContext(canvas);  if (!gl) {    console.log('Failed to get the rendering context for WebGL');    return;  }  // Initialize shaders  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {    console.log('Failed to intialize shaders.');    return;  }  // Write the positions of vertices to a vertex shader  var n = initVertexBuffers(gl);  if (n < 0) {    console.log('Failed to set the positions of the vertices');    return;  }  // Specify the color for clearing <canvas>  gl.clearColor(0, 0, 0, 1);  var tick = function() {    draw(gl, n);   // Draw the triangle    requestAnimationFrame(tick, canvas); // Request that the browser calls tick  };  tick();}function draw(gl, n) {  // Clear <canvas>  gl.clear(gl.COLOR_BUFFER_BIT);  initVertexBuffers(gl)    // Draw the rectangle  gl.drawArrays(gl.LINE_STRIP, 0, n);}function fi(x){    return Math.cos(x);}function ksi(x){    return Math.sin(3*x);}function Dalamber(x,t){    return 0.5*(fi(x-t) + fi(x+t))+0.5*(ksi(x+t)-ksi(x-t));}function initVertexBuffers(gl) {  var vertices = new Float32Array(400);  var i=0;  for(var t=-1.0;t<1.0;t+=0.01)  {      vertices[i]=t;      vertices[i+1]=Dalamber(t,xx)/2;      //document.write("x=",vertices[i],"y=",vertices[i+1]+"<br>");      i+=2;  }  xx+=0.01;  var n = vertices.length/2;  // Create a buffer object  var vertexBuffer = gl.createBuffer();  if (!vertexBuffer) {    console.log('Failed to create the buffer object');    return -1;  }  // Bind the buffer object to target  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);  // Write date into the buffer object  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');  if (a_Position < 0) {    console.log('Failed to get the storage location of a_Position');    return -1;  }  // Assign the buffer object to a_Position variable  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);  // Enable the assignment to a_Position variable  gl.enableVertexAttribArray(a_Position);  return n;}

Можете поэкспериментировать с функциями fi и ksi, изменять параметры и добавлять другие функции, делать из них сумму, умножать.

Результат:

Не судите строго и благодарю за внимание!

Подробнее..

Есть ли жизнь без WebGL 2.0?

16.03.2021 00:10:54 | Автор: admin

WebGL 2.0 вышел в далёком 2017ом году, принёс графический стек OpenGL ES 3.0 (2012го года), и, казалось бы, все современные браузеры давно должны были его поддерживать. Однако, среди лидеров затесались отстающие, и пользователи Safari до сих пор (начало 2021го) вынуждены ограничиваться возможностями WebGL 1.0, опубликованным в 2011ом году на основе OpenGL ES 2.0.

Те разработчики, что сталкивались с OpenGL ES 2.0, знают не понаслышке насколько ограниченным является этот графический стек. Ограничения программного интерфейса во многом отражали немощность мобильных графических карт того времени, поэтому массовый переход Android устройств на OpenGL ES 3.0 несколько лет назад оказался очень кстати, хоть и начался с серьёзным запозданием от десктопных видеокарт.

Браузерные технологии оказались ещё более инертными - в то время как Android устройства уже давно поддерживают OpenGL ES 3.2 с вычислительными шейдерами, внедряют поддержку Vulkan, а разработчики web-стандартов готовят WebGPU, обычному разработчику по-прежнему доступен устаревший уже на момент публикации WebGL 2.0, а то и вовсе WebGL 1.0 каменного века

Некоторое время назад, графический движок C++ фреймворка Open CASCADE Technology (OCCT) обзавёлся поддержкой PBR (основанной на физике освещению) модели материалов металл-шероховатость (metal-roughness). Продвижение формата glTF 2.0, известного как JPEG для 3D, способствовало выводу этой модели материалов как стандарта обмена между графическими движками.

PBR освещение требует существенно больше вычислительных ресурсов по сравнению с устаревшими эмпирическими моделями Гуро/Фонга времён OpenGL 1.1. Поэтому неудивительно, что реализация PBR в движке была изначально написана на основе относительно современного графического стека OpenGL 3.0 (выпущенного в 2008ом году!) и адаптированного под его мобильную версию OpenGL ES 3.0.

Однако тестирование графического движка в браузерах (в виде модуля WebAssembly) выявило уже озвученную аномалию - браузерный движок WebKit в основе одного из самых распространённых браузеров Safari до сих пор полноценно не поддерживает WebGL 2.0! И если пользователи Safari на macOS могут отделаться лёгким шоком и установить браузер посовременнее, то пользователи iOS такой возможности лишены политикой Apple. В AppStore можно найти альтернативные браузеры, но к сожалению все они основаны на том же движке WebKit, встроенном в систему, что и Safari - в силу ограничений магазина AppStore у разработчиков других браузеров просто нет выбора.

При этом вполне очевидно, что ограничение WebGL 1.0 связаны именно с программной стороной, ведь графические процессоры мобильных устройств Apple давно считаются относительно производительными, да и OpenGL ES 3.0 поддерживается на iOS нативными приложениями.

Более того, уже некоторое продолжительное время Safari имеет опцию, активирующую экспериментальную поддержку WebGL 2.0. На практике, экспериментальная опция всё ещё не проходит ряд важных тестов, хотя прогресс на лицо - WebGL 2.0 уже почти работает в iOS 14. И действительно, с локальным патчем для Emscripten, обходящим баги реализации экспериментального WebGL 2.0, мне удалось увидеть пример OCCT с работающим PBR освещением:

 function _glUniform4fv(location, count, value) {   GL.validateGLObjectID(GL.uniforms, location, 'glUniform4fv', 'location');   if (GL.currentContext.version >= 2) {      // WebGL 2 provides new garbage-free entry points to call to WebGL.      // Use those always when possible.-     GLctx.uniform4fv(GL.uniforms[location], HEAPF32, value>>2, count*4);-     return;+     //GLctx.uniform4fv(GL.uniforms[location], HEAPF32, value>>2, count*4);+     //return;   }...

Мини-вызов: OCCT PBR на WebGL 1.0

Несмотря на многочисленные свидетельства того, что Safari вот-вот обзаведётся поддержкой WebGL 2.0, текущие пользователи по-прежнему страдают от его отсутствия (ну или радуются экономии заряда батареи). Некоторые графические движки прямо заявляют, что не поддерживают PBR освещение без WebGL 2.0, однако мне стало любопытно, реалистично ли запустить PBR на WebGL 1.0 и с какими ограничениями.

Впрочем, конечной, целью была выбрана не поддержка голого WebGL 1.0, а запуск PBR на современных устройствах iPad с доступными расширениями WebGL. Вот список таких расширений для устройства iPad 2020 (Apple A12 Bionic) на iOS 14.4 / Safari:

EGLVersion: 1.4 Emscripten EGLEGLVendor: EmscriptenEGLClientAPIs: OpenGL_ESGLvendor: WebKitGLdevice: WebKit WebGLGLunmaskedVendor: Apple Inc.GLunmaskedDevice: Apple GPUGLversion: OpenGL ES 2.0 (WebGL 1.0)GLSL: OpenGL ES GLSL ES 1.00 (WebGL GLSL ES 1.0 (1.0))Max texture size: 16384Max FBO dump size: 16384x16384Max combined texture units: 32Viewport: 1560x1080GLextensions: GL_EXT_blend_minmax GL_EXT_sRGB GL_OES_texture_floatGL_OES_texture_half_float GL_OES_texture_half_float_linearGL_OES_standard_derivatives GL_EXT_shader_texture_lodGL_EXT_texture_filter_anisotropic GL_OES_vertex_array_objectGL_OES_element_index_uint GL_WEBGL_lose_context GL_WEBGL_compressed_texture_astcGL_WEBGL_compressed_texture_etc GL_WEBGL_compressed_texture_etc1GL_WEBKIT_WEBGL_compressed_texture_pvrtc GL_WEBGL_depth_textureGL_ANGLE_instanced_arrays GL_WEBGL_debug_shaders GL_WEBGL_debug_renderer_infoGL_EXT_color_buffer_half_float

Отладка вёб-приложения в мобильном браузере удовольствие весьма сомнительное, поэтому первым делом были подобраны альтернативные конфигурации с WebGL 1.0:

  • Отключение WebGL 2.0 в скрытых опциях Firefox.

    • Управляется опцией webgl.enable-webgl2=false на странице about:config.

    • Предоставляет аппаратно-ускоренную реализацию WebGL 1.0, допускающую некоторые отклонения от WebGL 1.0 спецификаций на железе уровня WebGL 2.0.

    • Поддерживает вывод JavaScript консоли.

  • Отключение WebGL 2.0 в опциях сборки Emscripten.

    • Управляется флагом сборки MAX_WEBGL_VERSION=1.

    • Предоставляет аппаратно-ускоренную реализацию WebGL 1.0.

    • Поддерживает JavaScript консоль (в десктопных браузерах).

  • Отключение аппаратного ускорения в браузерах на движке Chromium в паре с опцией сборки MAX_WEBGL_VERSION=1.

    • Предоставляет программную реализацию WebGL 1.0, которая обычно ближе придерживается к спецификациям по сравнению с аппаратно-ускоренными реализациями.

    • Поддерживает JavaScript консоль (в десктопных браузерах).

  • Сборка Draw Harness на десктопе с опцией OpenGL ES, реализованной библиотекой ANGLE.

    • Использует ту же реализацию OpenGL ES, которую используют десктопные браузеры.

    • Команда vcaps -maxversion 2 0 активирует создание OpenGL ES 2.0 контекста (вместо OpenGL ES 3.0).

    • Очень полезный и удобный вариант для отладки, однако поведение не идентично вёб-приложению с дополнительным уровнем WebGL реализации.

  • Запуск в браузере Safari на macOS.

    • Предоставляет аппаратно-ускоренную реализацию WebGL 1.0.

    • Поведение на Apple M1 (ARM64) очень близко в iPad, но есть расхождения!

    • Поддерживает вывод JavaScript консоли.

  • Запуск в браузере Safari на iOS.

    • Предоставляет аппаратно-ускоренную реализацию WebGL 1.0.

    • Нет JavaScript консоли.

Разные комбинации дают отличные результаты - разный набор расширений, разные ошибки, разные баги а в сумме много полезной информации для отладки. Прослеживается одна характерную особенность WebGL реализаций - там где проприетарные драйвера OpenGL и OpenGL ES допускают отклонения, в случае если железо в принципе поддерживает какую-то функциональность, реализации WebGL упрямо стоят на своём и ругаются на синтаксические ошибки в коде GLSL.

Наиболее упрямыми реализациями оказались программный WebGL, реализуемый средствами библиотеки ANGLE, а также реализация OpenGL поверх Metal от Apple. Там где CG компилятор NVIDIA не скажет ни слова, драйвер AMD мягко предупредит в логе компиляции шейдера, OpenGL реализация Apple не оставит безобразие без внимания и ошибкой скажет, что такой функции в GLSL 110 нет и появилась она только в GLSL 120!

Портирование кода PBR на WebGL 1.0 было встречено следующими проблемами:

  • Загрузка данных PBR таблицы-кеша 128x128 формата GL_RG32F в текстуру формата GL_RG16F.

    • Проблема #1: текстурные форматы GL_RG32F/GL_RG16F не поддерживаются iPad + WebGL 1.0 (расширение GL_EXT_texture_rg недоступно).

    • Проблема #2: текстуры формата GL_RGBA32F не поддерживают фильтрацию на iPad + WebGL 1.0. iPad не поддерживает расширение GL_OES_texture_float_linear, однако нефильтруемые текстуры с плавающей запятой поддерживается через расширение GL_OES_texture_float. В тоже время, iPad поддерживает расширение GL_OES_texture_half_float_linear, так что текстуры с плавающей точкой половинчатой точности поддерживают фильтрацию.

    • Проблема #3: текстуры формата GL_RGBA16F могут быть загружены напрямую из данных плавающей запятой одинарной точности в случае с OpenGL ES 3.0 / WebGL 2.0, однако WebGL 1.0 + GL_OES_texture_half_float не допускает этого.

  • Запекание спекулярной PBR карты в текстуру 9x1 GL_RGBA32F.

    • Проблема: в текстуру формата GL_RGBA32F нельзя производить отрисовку через FBO на iPad + WebGL 1.0. iPad не поддерживает расширение WEBGL_color_buffer_float.

  • Запекание мип-уровней диффузной PBR кубической текстуры 512x512x6 GL_RGBA8.

    • Проблема: iPad + WebGL 1.0 не допускают отрисовку в мип-уровни, отличные от нулевого (расширение GL_OES_fbo_render_mipmap не поддерживается).

  • PBR GLSL программы полагаются на явное задание мип-уровня текстуры, в зависимости от шероховатости материала.

    • Проблема: textureCubeLod() недоступна в GLSL 100 es, но доступна посредством расширения GL_EXT_shader_texture_lod на iPad + WebGL 1.0.

  • PBR GLSL программы содержат большие блоки циклов, ветвления и оператор модуля %.

    • Проблема #1: оператор модуля % недоступен в GLSL 100 es, но может быть заменён функцией mod().

    • Проблема #2: GLSL 100 es не предусматривает синтаксиса для инициализации массива констант.

    • Проблема #3: GLSL 100 es не допускает неконстантные выражения для определения индекса (non-constant index expressions).

  • Буфер цвета sRGB и кубическая карта окружения.

    • Проблема #1: расширение GL_EXT_sRGB доступно на iPad + WenGL 1.0, но требует иных констант для инициализации, а также запрещает генерацию мип-уровней посредством glGenerateMipmap().

    • Проблема #2: ужасно медленная генерация мип-уровней для sRGB текстур на WebGL 2.0 (5 секунд!).

Поиск решений

Случайное использование нефильтруемые форматы текстур - одна из самых противных проблем в OpenGL. Логи выглядят абсолютно чистыми - ни ошибок glGetError(), ни предупреждений WebGL в консоли, просто на экран выводится некорректный результат. С практической точки зрения, нефильтруемая текстура возвращает нули в GLSL программу, и требуется приличное время для того, чтобы обнаружить источник проблемы.

Загрузка GL_RGBA16F текстуры в случае с WebGL 1.0 + GL_OES_texture_half_float требует программной реализации конвертера 32битных чисел с плавающей запятой в 16битные - ведь центральные процессоры и C/C++ не имеют встроенной поддержки чисел с половинной точностью. OpenGL 3.0 и OpenGL ES 3.0 позволяют избежать этого чудного кода, а вот для поддержки WebGL 1.0 придётся его добавить в приложение. В процессе отладки удалось запечатлеть вот такой забавный эффект при интерпретации GL_RGBA32F данных как массива GL_RGBA16F:

Невозможность отрисовки в текстуру формата GL_RGBA32F стала неприятной проблемой для реализации PBR, так как меньшая точность будет недостаточно для данной текстуры. К счастью, PBR спекулярная карта имеет размер всего 9x1 текселей - можно было бы даже подумать о вычислении значений без помощи OpenGL, если бы это не тянуло за собой необходимость реализовать выборки с фильтрацией из кубической текстуры Вместо этого, следующий подход был реализован: значения с плавающей запятой упаковываются шейдером в текстуру формата 9x3 GL_RGBA8 (по строке на RGB компоненту), затем читаются с посредством glReadPixels(), распаковываются и загружаются в финальную текстуру формата GL_RGBA32F.

Заполнение мип-уровней PBR диффузной кубмапы также заставило задуматься, но обходной путь оказался проще - отрисовка во временную текстуру и копирование результата в нужный мип-уровень посредством glCopyTexImage2D(). Как ни странно, использование нулевого мип-уровня той же самой текстуры сработало, хотя не могу с уверенностью сказать, что такая логика не чревата неопределённым поведением.

Без функции textureCubeLod() в шейдере практически невозможно реализовать корректное поведение PBR освещение с разными уровнями шероховатости, но к счастью, все тестируемые реализации WebGL 1.0 поддерживали расширение GL_EXT_shader_texture_lod, активируемое в GLSL шейдере кодом:

+#extension GL_EXT_shader_texture_lod : enable+#define textureCubeLod textureCubeLodEXT

Пара скриншотов внизу показывает как бы выглядели PBR материалы, если просто заменить textureCubeLod() на textureCube(), т.е. на автоматический выбор мип-уровней текстуры вместо ручного на основании шероховатости:

Многочисленные ограничения синтаксиса GLSL 100 es не раз заставляли задумываться о напрасной борьбе с тенью прошлого. И если оператор модуля % легко заменяется на функцию mod(), а прочие ограничения могут ввести в ступор:

  • Ранние версии GLSL просто не предусматривали синтаксис для инициализации массива констант:
    > const float aSHBasisFuncCoeffs[9] = float[9] { 0.0, 1.0, 2.0, };

    • Выходом из этой проблемы послужило объявление таких массивов как uniform переменных, загружаемых из C/C++ кода - что, в общем-то, не тоже самое, что константа уровня компиляции, но по производительности может быть близка к этому.

  • Переменное количество итераций цикла for(;;). Программа запекания PBR текстур задаёт несколько параметров, задающих точность (качество):
    > uniform int uSamplesNum;
    > for (int aSampleIter = 0; aSampleIter < uSamplesNum; ++aSampleIter) {}

    • Большее количество выборок увеличивают качество, но требуют более тяжёлых расчётов, поэтому эти параметры были вынесены в настройки, и более того, автоматически калибруются в зависимости от мип-уровня текстуры. Ограничения GLSL 100 es нарушают эту логику - uniform переменная должна быть заменена на константу компиляции. Хотя типичным обходным путём может быть также написание такого цикла:
      > uniform int uSamplesNum;
      > int TheMaxSamples = 1024;
      > for (int aSampleIter = 0; aSampleIter < TheMaxSamples; ++aSampleIter) {
      > if (aSampleIter >= uSamplesNum) { break; }
      > }

  • Non-constant index expressions are disallowed. Программа запекания PBR карт использует такие конструкции:
    > int anIndex = int(gl_FragCoord.x);
    > float aCoef = aSHCosCoeffs[anId] * aSHBasisFuncCoeffs[anId];

    • Ограничения GLSL 100 es приводят к написанию следующего ужасного кода с использованием if/else.
      > if (anId == 0) { aCoef = aSHCosCoeffs[0] * aSHBasisFuncCoeffs[0]; }
      > else if (anId == 1) { aCoef = aSHCosCoeffs[1] * aSHBasisFuncCoeffs[1]; }
      > else if (anId == 2) { aCoef = aSHCosCoeffs[2] * aSHBasisFuncCoeffs[2]; }

Большинство ограничений языка шейдеров GLSL 100 es являются не более чем эхом графических процессоров прошлого, не поддерживающих ветвление. И хотя синтакс GLSL заделывался на будущее, первая версия требовала написание циклов и условий ветвлений, которые могли быть тривиально раскручены (trivially unrolled) компилятором, т.к. графические процессоры того времени иного варианта просто не поддерживали.

sRGB текстуры

Рендеринг с учётом цветового пространства sRGB важен для корректной цветопередачи. К сожалению, WebGL 1.0 + GL_EXT_sRGB имеет существенное ограничение - невозможность генерации мип-уровней. Это делает поддержку sRGB текстур практически бесполезной, если только не использовать форматы с предварительно подготовленными мип-уровнями. И если в случае обычных текстур мип-уровнями можно пожертвовать (снизится качество картинки), то при запекании PBR карт из кубмапы окружения просто обойтись без мип-уровней уже нельзя.

Но даже в случае с WebGL 2.0, формально поддерживающим генерацию мип-уровней sRGB текстур, данная функциональность реализована чудовищно медленной. Генерация мип-уровней кубмапы размером 2048x2048x6 выполняется 5 секунд на быстром десктопном компьютере! При этом тот же самый код отрабатывает за считанные доли секунд при использовании нативной OpenGL / OpenGL ES реализации вместо браузера.

Всё указывает на то, что WebGL реализует какой-то код не на графическом процессоре - возможно с целью обеспечения строгих условий спецификаций WebGL в контексте конвертации цветового пространства sRGB, которым аппаратные реализации не следуют, хотя мне и не удалось найти какие-либо упоминания на эту тему. По всей видимости, в WebGL лучше избегать sRGB текстуры (ценой качества картинки или искажений цветового пространства), обеспечить загрузку предварительно подготовленных мип-уровней текстур, или реализовать генератор mip-map уровней собственной шейдерной программой.

Послесловие

В результате потраченных усилий наконец-то удалось увидеть PBR материалы посредством графического движка OCCT в Safari на iPad c контекстом WebGL 1.0. Конечно, тестируемое устройство относится к относительно новому поколению (iPad 2020, основанному на Apple A12 SoC анонсированной в 2018ом году), но есть надежда на то, что более старые устройства Apple также справятся с задачей.

Хочется верить, что уже в скором времени WebGL 2.0 наконец-то станет минимальным стандартом и необходимость в поддержке допотопных технологий десятилетней давности отпадёт - многие разработчики уже несколько лет отказывают поддерживать такие браузеры и их решение легко понять.

В этом контексте, отказ Microsoft от поддержки устаревших движков вроде Internet Explorer и Microsoft Edge Legacy (базирующимся на не-Chromium движке) ощущается как свежий воздух для вёб-разработчика, измученного проблемами совместимости. Хотя исчезающе малое количество конкурирующих полноценных вёб-движков не может не настораживать (Mozilla Firefox, Chromium, Safari/WebKit) и опасаться за будущее открытых вёб-стандартов в мире, где один браузер станет бесконтрольно доминировать.

Стратегия Apple по удержанию экосистемы iOS под колпаком и не пускать решения конкурентов лишает пользователей системы собственного выбора. Многочисленные слухи свидетельствуют, что измученная экспериментальная поддержка WebGL 2.0 в движке WebKit вот-вот перестанет быть экспериментальной (в том числе благодаря переходу на реализацию OpenGL ES библиотекой ANGLE, которая уже давно используется другими браузерами), хотя сроки, как обычно, остаются неизвестными.

Оригинальную публикацию на английском можно найти здесь.

Подробнее..

Membrane game шикарная игра для аутистов в 20 строк кода

11.04.2021 14:15:43 | Автор: admin

https://www.shadertoy.com/view/fs23Wt
Код игры написан целиком на языке математики. Давайте его разберем.

  1. N отвечает за размер клеток.

  2. pow(1.02, iTime) создает равномерную анимацию клеток, 1.02 - скорость анимации; рано или поздно это вызывет переполнение буфера, но анимаию можно отключить сделав t=1 или сделать так чтобы она замедлялась со временем t=iTime.

  3. dx и dy - смещение позиции курсора по x и по y.

  4. float f = float((x-dx)*(x-dx)t+(y-dy)*(y-dy)*t); // Это правила игры - в эту функцию (на самом деле не функцию) вы можете внести изменения и получить новую логику игры.

  5. float F = abs(f*sin((x)/N)*sin((y)/N)); // Создает клеточную мембрану для функции (не функции) f.

  6. int R = int(floor(F*pow(16.0, 6.0-ceil(log2(F)/4.0)))); // Адаптирует цвета мембраны добавив нули в конце шестнадцатиричной записи числа или обрезае его по правому краю. По сути это готовый индекс цвета X11, который используется в HTML или Photoshop.

Короче всё просто. Я на самом деле новичок и в математике и в GLSL, но моим друзьям понравилась эта игра, и мне интересно, как её оценит хабр. Если у вас есть идеи, вы можете форкнуть. Тех, кто захочет попробовать перенести на другие языки программирования, предупрежу, что такие вычисления для каждого пикселя весьма затруднительны для процессора - надо программировать шейдеры (ну лично мой опыт, но вообще я не против.)

Подробнее..

Recovery mode Часы и волны

12.05.2021 22:22:20 | Автор: admin

Я показывал хабру свою графику, хотя меня и заминусовали, я хочу показать снова то, над чем я работал, может кому то из большой аудитории хабра понравится анимация, графика или идея, которая уже давно лежит на поверхности. Это прототип стрелочных часов, измеряющих время в 7300000-7320000 больших размерах, может столько и не нужно, но обычным 12-часовым часам явно не хватает делений для измерения хотя бы сегодняшней даты.

Я добавил красивую графику, чтоб было не слишком скучно: получилось что часы сущесвуют в пространстве, зависящем от t1 (текущие дата и время), а окружающие фракталы в пространстве, вычисляя цвет пикселей, зависят от t2 (время прошедшее с момента запуска) - мне нравится эта особенность, она кажется странной.

Картинка красивая, но лучше посмотреть работу часов, и их анимацию тут; исходники прилагаются.

Прототип часов

Вернемся к прототипу часов. Тут нет цифр, но к этому быстро привыкаешь. Хочу обратить внимание на другие качественные особенности: вывод месяца и даты (зеленая дуга), визуализаия четырехзначного номера года (4 серых дуги для обозначения каждой цифры текущего года, расположенные под углом 90 градусов по отношению к друг-другу, их может быть меньше, если в номере года встречаются нули). А также первая и вторая половина (pm/am) суток явно указываются часовой срелкой (на темное или светлое поле) - это обеспечивается вращающимся циферблатом.

Я не понимаю, почему когда когда круглый цифреблат часов способен вмещать в себя день, месяц, год, - почему в массовой продаже не выпускаются такие часы, более точные? Если речь идет о стрелочных часах, то даже за большие деньги вам предложат скорее всего пару дополнительных циферблатов в лучшем случае - но если мы часы, минуты, секунды меряем по одному и тому же циферблату, почему другие единицы того же времени используют дополнительные циферблаты?

Мне нравится такая реализация, я не понимаю почему таких штук нет в продаже. Мне кажется, такие календарные часы могли бы вытеснить устаревшие версии, которые годятся к использованию только вместе с календарем.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru