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

Shaders

Перевод Визуализация списка женщин-лауреатов Нобелевской премии в виде кристаллов в 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 демонстрацией результата:



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

Подробнее..

Перевод Как можно использовать шейдеры в Android View и как Android View может использовать шейдеры

22.03.2021 18:15:22 | Автор: admin

Перевод статьи подготовлен в преддверии старта курсов "Android Developer. Basic" и "Android Developer. Professional".


Для полноценной работы с динамическими элементами пользовательского интерфейса, которые используют тени, фильтры в реальном времени для фото или видео, или адаптивный пользовательский интерфейс и освещение, недостаточно использовать только Canvas. Было бы куда лучше, если бы у нас в распоряжении было что-то помощнее. Раньше мы могли использовать RenderScript, но поддерживается ли он сейчас?

В этой статье я расскажу как использовать стандартные GLSL шейдеры OpenGL в вашем пользовательском view, которое является наследником класса Android View (android.view.View). Я предлагаю вам использовать это решение, если вы работаете над чем-нибудь из нижеперечисленного:

  • Шейдеры или коррекция цвета в реальном времени для видеопотоков.

  • Динамические тени и освещение для кастомных элементов пользовательского интерфейса.

  • Продвинутая попиксельная анимация.

  • Какие-либо эффекты пользовательского интерфейса, наподобие размытия (blurring), искажения (distortion), пикселизации и т. д.

  • Если вы создаете новый нейроморфный адаптивный пользовательский интерфейс.

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

Идея

Нам нужно, чтобы в нашем стандартном лэйауте лежал класс, который ведет себя так же, как Android View (android.view.View), и мы cможем использовать фрагментный шейдер OpenGL для визуализации его содержимого.

Демо

Демо-приложение с несколькими ShaderViews. Динамический свет и видео фильтры.

Как это работает на абстрактном примере

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

  • Волшебные краски GLSL шейдеры OpenGL.

  • Холст четырехугольник, который заполнит все пространство нашего кастомного view.

  • Известный художник класс, реализующий интерфейс Render. Этот художник, в свою очередь, использует волшебные краски, чтобы нарисовать картину на холсте.

  • Картина кастомный view-класс, который задействует художника с его/ее холстом и волшебными красками.

  • Стена Activity или Fragment android.

Как это работает с технической точки зрения

  1. Давайте выберем родительский view для нашего кастомного view-класса (кстати, мы назовем наш view-класс ShaderView). Тут у нас есть два варианта: SurfaceView и TextureView. Я вернусь к разнице между ними через пару мгновений.

  2. Создадим класс Render, который будет отображать view с использованием шейдеров.

  3. Создадим 3D-модель четырехугольника (quadrangle), который заполнит все пространство view (3D, поскольку OpenGL был создан для 3D-сцен). Не беспокойтесь об этом; это стандартное решение, и с ним не связано никаких трудностей.

Четырехугольник OpenGL внутри TextureView.Четырехугольник OpenGL внутри TextureView.

SurfaceView или TextureView

SurfaceView и TextureView оба наследуются от класса Android View, но между ними есть некоторые различия.

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

TextureView ведет себя как обычный android.view.View, и вы можете анимировать, преобразовывать, масштабировать или даже наслаивать его с другими экземплярами. Но это преимущество дается на ценой потребления большего количества памяти, чем SurfaceView, и вы теряете в производительности (в среднем 13 кадра).

Возвращаясь к сути вопроса, поскольку мы хотели, чтобы наше кастомное view вело себя как обычное view Android, мы должны использовать TextureView.

Следующая проблема для нас заключается в том, что нет встроенного класса, который использует OpenGL render и TextureView. Но не спешите расстраиваться GLSurfaceView подходит как раз для того, что нам нужно, но только с SurfaceView, поэтому давайте поразмыслим о том, как мы можем использовать этот класс для нашего собственного GLTextureView.

Создание GLTextureView

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

  1. Создайте новый класс GLTextureView.kt, который наследуется от TextureView и расширяет TextureView.SurfaceTextureListener и View.OnLayoutChangeListener. Добавьте конструкторы.

open class GLTextureView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) :    TextureView(context, attrs, defStyleAttr),    TextureView.SurfaceTextureListener,    View.OnLayoutChangeListener {}

GLTextureView.kt на GitHub

5. Обновите метод finalize() до стандарта Kotlin. (Если у вас есть лучшее решение, напишите в комментариях).

6. Замените SurfaceHolder на SurfaceTexture.

7. Замените все упоминания GLSurfaceView на GLTextureView.

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

9. Устранение проблемы с допустимостью нулевых значений после автоматического преобразования кода Java в Kotlin. В моем случае мне пришлось обновить методы переопределения и некоторые параметры, допускающие значение NULL (например, egl: EGL10 должно быть egl: EGL10?).

10. Переместите константы в объект-компаньон или на верхний уровень.

11. Удалите неподдерживаемые аннотации.

12. Добавьте методы интерфейса SurfaceTextureListener.

 override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {        surfaceCreated(surface)        surfaceChanged(surface, 0, width, height)    }    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {        surfaceChanged(surface, 0, width, height)    }    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {        surfaceDestroyed(surface)        return true    }    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {    }

GLTextureView.kt на GitHub

13. В createSurface() вы наткнетесь на неработающую строчку, замените view.holder на view.surfaceTexture.

14. Переопределите onLayoutChange.

 override fun onLayoutChange(        v: View?, left: Int, top: Int,        right: Int,        bottom: Int,        oldLeft: Int,        oldTop: Int,        oldRight: Int,        oldBottom: Int    ) {        surfaceChanged(surfaceTexture, 0, right - left, bottom - top)    }

GLTextureView.kt на GitHub

В результате у вас получится что-то вроде этого.

Расширения

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

fun Resources.getRawTextFile(@RawRes resource: Int): String =   openRawResource(resource).bufferedReader().use { it.readText() }

extensions.kt на GitHub

Код шейдеров

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

Вершинный шейдер (Vertex Shader)

Для наших целей нам достаточно простого вершинного шейдера для рендеринга нашего четырехугольника (мы не потратим кучу времени на его код).

#version 300 esuniform mat4 uMVPMatrix;uniform mat4 uSTMatrix;in vec3 inPosition;in vec2 inTextureCoord;out vec2 textureCoord;void main() {   gl_Position = uMVPMatrix * vec4(inPosition.xyz, 1);   textureCoord = (uSTMatrix * vec4(inTextureCoord.xy, 0, 0)).xy;}

vertex.vsh на GitHub

Фрагментный/пиксельный шейдер (Fragment Shader)

Код довольно прост, но давайте посмотрим, что у нас здесь есть.

Прежде всего, мы определяем версию GLSL.

#version 300 es

Затем мы определяем пользовательские параметры, которые мы собираемся отправить шейдеру.

uniform vec4 uMyUniform;

Определяем параметры ввода и вывода для нашего фрагментного шейдера. In что мы получаем от вершинного шейдера (в нашем случае координаты текстуры), а out что отправляем в результате (цвет пикселя).

in vec2 textureCoord;out vec4 fragColor;

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

void main() {   fragColor = vec4(textureCoord.x, textureCoord.y, 1.0, 1.0) * uMyUniform;}

В результате мы получим следующее:

#version 300 esprecision mediump float;uniform vec4 uMyUniform;in vec2 textureCoord;out vec4 fragColor;void main() {    fragColor = vec4(textureCoord.x, textureCoord.y, 1.0, 1.0) * uMyUniform;}

fragment_shader.fsh на GitHub

QuadRender

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

Четырехугольник OpenGL в проекции камеры. Камера это точка зрения пользователя, который смотрит на устройство.

Наш класс должен расширить интерфейс GLTextureView.Renderer тремя методами:

onSurfaceCreated() Создает программу шейдера, связывает некоторые параметры формы (uniform) и отправляет атрибуты в вершинный шейдер.

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

onSurfaceChanged() Обновляет вьюпорт.

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

Определите константы.

private const val FLOAT_SIZE_BYTES = 4 // размер Floatprivate const val TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES // 5 floatов для каждой вершины (3 floatа на позицию и 2 на координаты текстуры)private const val TRIANGLE_VERTICES_DATA_POS_OFFSET = 0 // позиция начинается с начала массива каждой вершиныprivate const val TRIANGLE_VERTICES_DATA_UV_OFFSET = 3 // координаты текстуры начиная с 3-го floatа (4-й и 5-й floatы)// атрибуты вершинного шейдераconst val VERTEX_SHADER_IN_POSITION = "inPosition"const val VERTEX_SHADER_IN_TEXTURE_COORD = "inTextureCoord"const val VERTEX_SHADER_UNIFORM_MATRIX_MVP = "uMVPMatrix"const val VERTEX_SHADER_UNIFORM_MATRIX_STM = "uSTMatrix"const val FRAGMENT_SHADER_UNIFORM_MY_UNIFORM = "uMyUniform"private const val UNKNOWN_PROGRAM = -1private const val UNKNOWN_ATTRIBUTE = -1

QuadRender.kt на GitHub

Две переменные, которые будут содержать исходный код наших вершинного и фрагментного шейдеров.

private var vertexShaderSource : String, // исходный код вершинного шейдераprivate var fragmentShaderSource : String, // исходный код фрагментного шейдераQuadRender.kt на GitHub

Определите список вершин для буфера вершин.

private val quadVertices: FloatBufferinit {// задаем массив вершин четырехугольникаval quadVerticesData = floatArrayOf(// [x,y,z, U,V]-1.0f, -1.0f, 0f, 0f, 1f,1.0f, -1.0f, 0f, 1f, 1f,-1.0f, 1.0f, 0f, 0f, 0f,1.0f, 1.0f, 0f, 1f, 0f)quadVertices = ByteBuffer.allocateDirect(quadVerticesData.size * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer().apply {put(quadVerticesData).position(0)}}

QuadRender.kt на GitHub

Определите матрицы.

private val matrixMVP = FloatArray(16)private val matrixSTM = FloatArray(16)

QuadRender.kt на GitHub

И добавить инициализацию в init{} блок.

init {// код, который мы добавили ранееMatrix.setIdentityM(matrixSTM, 0)}

QuadRender.kt на GitHub

Вершинный шейдер, атрибуты вершин и расположение матриц.

private var inPositionHandle = UNKNOWN_ATTRIBUTEprivate var inTextureHandle = UNKNOWN_ATTRIBUTEprivate var uMVPMatrixHandle = UNKNOWN_ATTRIBUTEprivate var uSTMatrixHandle = UNKNOWN_ATTRIBUTEprivate var uMyUniform = UNKNOWN_ATTRIBUTE

QuadRender.kt на GitHub

Локатор программы шейдера.

private var program = UNKNOWN_PROGRAM

QuadRender.kt на GitHub

Отлично, мы закончили с инициализацией. Теперь давайте напишем метод onSurfaceCreated(). Мы загрузим и инициализируем наши шейдеры и получим указатели для атрибутов, включая параметр формы uMyUniform, который мы будем использовать для отправки некоторых пользовательских векторных данных во фрагментный шейдер.

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {//создаем программу шейдера из исходного кодаcreateProgram(vertexShaderSource, fragmentShaderSource)// связываем вектор атрибутов шейдераinPositionHandle = GLES20.glGetAttribLocation(program, VERTEX_SHADER_IN_POSITION)checkGlError("glGetAttribLocation $VERTEX_SHADER_IN_POSITION")if (inPositionHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get attrib location for $VERTEX_SHADER_IN_POSITION")inTextureHandle = GLES20.glGetAttribLocation(program, VERTEX_SHADER_IN_TEXTURE_COORD)checkGlError("glGetAttribLocation $VERTEX_SHADER_IN_TEXTURE_COORD")if (inTextureHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get attrib location for $VERTEX_SHADER_IN_TEXTURE_COORD")uMVPMatrixHandle = GLES20.glGetUniformLocation(program, VERTEX_SHADER_UNIFORM_MATRIX_MVP)checkGlError("glGetUniformLocation $VERTEX_SHADER_UNIFORM_MATRIX_MVP")if (uMVPMatrixHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get uniform location for $VERTEX_SHADER_UNIFORM_MATRIX_MVP")uSTMatrixHandle = GLES20.glGetUniformLocation(program, VERTEX_SHADER_UNIFORM_MATRIX_STM)checkGlError("glGetUniformLocation $VERTEX_SHADER_UNIFORM_MATRIX_STM")if (uSTMatrixHandle == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get uniform location for $VERTEX_SHADER_UNIFORM_MATRIX_STM")// (!) связываем атрибуты фрагментного шейдераuMyUniform = GLES30.glGetUniformLocation(program, FRAGMENT_SHADER_UNIFORM_MY_UNIFORM)checkGlError("glGetUniformLocation $FRAGMENT_SHADER_UNIFORM_MY_UNIFORM")if (uMyUniform == UNKNOWN_ATTRIBUTE) throw RuntimeException("Could not get uniform location for $FRAGMENT_SHADER_UNIFORM_MY_UNIFORM")}

QuadRender.kt на GitHub

Обратите внимание на последние три строки, где мы получаем расположение нашей кастомной формы (uMyUniform) для фрагментного шейдера. Для более сложных шейдеров нам придется добавить больше таких параметров.

В onSurfaceCreated() мы использовали специальные методы для создания и связывания программы.

/*** Создаем программу шейдера из исходного кода вершинного и фрагментного шейдера*/private fun createProgram(vertexSource: String, fragmentSource: String): Boolean {if (program != UNKNOWN_PROGRAM) {// удаляем программуGLES30.glDeleteProgram(program)program = UNKNOWN_PROGRAM}// загружаем вершинный шейдерval vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource)if (vertexShader == UNKNOWN_PROGRAM) {return false}// загружаем фрагментный шейдерval pixelShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource)if (pixelShader == UNKNOWN_PROGRAM) {return false}program = GLES30.glCreateProgram()if (program != UNKNOWN_PROGRAM) {GLES30.glAttachShader(program, vertexShader)checkGlError("glAttachShader: vertex")GLES30.glAttachShader(program, pixelShader)checkGlError("glAttachShader: pixel")return linkProgram()}return true}private fun linkProgram(): Boolean {if (program == UNKNOWN_PROGRAM) {return false}GLES30.glLinkProgram(program)val linkStatus = IntArray(1)GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0)if (linkStatus[0] != GLES30.GL_TRUE) {Log.e(TAG, "Could not link program: ")Log.e(TAG, GLES30.glGetProgramInfoLog(program))GLES30.glDeleteProgram(program)program = UNKNOWN_PROGRAMreturn false}return true}private fun loadShader(shaderType: Int, source: String): Int {var shader = GLES30.glCreateShader(shaderType)if (shader != UNKNOWN_PROGRAM) {GLES30.glShaderSource(shader, source)GLES30.glCompileShader(shader)val compiled = IntArray(1)GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0)if (compiled[0] == UNKNOWN_PROGRAM) {Log.e(TAG, "Could not compile shader $shaderType:")Log.e(TAG, GLES30.glGetShaderInfoLog(shader))GLES30.glDeleteShader(shader)shader = UNKNOWN_PROGRAM}}return shader}private fun checkGlError(op: String) {var error: Intwhile (GLES30.glGetError().also { error = it } != GLES30.GL_NO_ERROR) {Log.e(TAG, "$op: glError $error")throw RuntimeException("$op: glError $error")}

QuadRender.kt на GitHub

Следующий метод, который мы должны реализовать, это onDrawFrame().

override fun onDrawFrame(gl: GL10?) {// очищаем наш "экран"GLES30.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT or GLES30.GL_COLOR_BUFFER_BIT)// используем программуGLES30.glUseProgram(program)// устанавливаем ввод шейдера (встроенные атрибуты)setAttribute(inPositionHandle, VERTEX_SHADER_IN_POSITION, 3, TRIANGLE_VERTICES_DATA_POS_OFFSET) // 3 потому что 3 floatа на позициюsetAttribute(inTextureHandle, VERTEX_SHADER_IN_TEXTURE_COORD, 2, TRIANGLE_VERTICES_DATA_UV_OFFSET) // 2 потому что 2 floatа на координаты текстуры// обновляем матрицуMatrix.setIdentityM(matrixMVP, 0)GLES30.glUniformMatrix4fv(uMVPMatrixHandle, 1, false, matrixMVP, 0)GLES30.glUniformMatrix4fv(uSTMatrixHandle, 1, false, matrixSTM, 0)// (!) обновляем формы для фрагментного шейдераval uMyUniformValue = floatArrayOf(1.0f, 0.75f, 0.95f, 1f) // некоторые значения, которые мы собираемся передать фрагментному шейдеруGLES30.glUniform4fv(uMyUniform, 1, uMyUniformValue, 0)// активируем смешивание текстур (для поддержки прозрачности)GLES30.glBlendFunc(GLES30.GL_SRC_ALPHA, GLES30.GL_ONE_MINUS_SRC_ALPHA)GLES30.glEnable(GLES20.GL_BLEND)// отрисовываем наши четырехугольникиGLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)checkGlError("glDrawArrays")GLES30.glFinish()}

QuadRender.kt на GitHub

Обратите внимание на строки, в которых мы отправляем кастомное значение (uMyUniformValue) в форму (uMyUniform) во фрагментный шейдер.

И последнее, surfaceChange() довольно простой метод.

override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {GLES30.glViewport(0, 0, width, height)}

QuadRender.kt на GitHub

Полный код этого класса вы можете найти здесь.

ShaderView

Отлично, все, что нам нужно для нашего ShaderView, готово. Теперь мы можем использовать мощь фрагментного шейдера для рендеринга его содержимого! Создадим ShaderView.

private const val OPENGL_VERSION = 3class ShaderView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0) :GLTextureView(context, attrs, defStyleAttr) {init {// определяем версию OpenGLsetEGLContextClientVersion(OPENGL_VERSION)// загружаем исходный код шейдеров из файловval vsh = context.resources.getRawTextFile(R.raw.vertex_shader)val fsh = context.resources.getRawTextFile(R.raw.fragment_shader)// устанавливаем рендерерsetRenderer(QuadRender(vertexShaderSource = vsh, fragmentShaderSource = fsh))// устанавливаем режим рендеринга RENDERMODE_WHEN_DIRTY или RENDERMODE_CONTINUOUSLYsetRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY) // или GLSurfaceView.RENDERMODE_CONTINUOUSLY если нужно обновлять его на каждом кадре}}

ShaderView.kt на GitHub

Дополнительно: Использование текстур в фрагментных шейдерах

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

Вам нужно определить форму во фрагментном шейдере как sampler2D и получить текущий пиксель текстуры по координатам текстуры с помощью метода texture() из GLSL.

Вот полный код шейдера.

#version 300 esprecision mediump float;uniform sampler2D uTexture;in vec2 textureCoord;out vec4 fragColor;void main() {fragColor = texture(uTexture, textureCoord);}

fragment_texture_shader.fsh на GitHub

Затем нам понадобятся два расширения для загрузки и использования растрового изображения в качестве текстур OpenGL.

fun Resources.loadBitmapForTexture(@DrawableRes drawableRes: Int): Bitmap {val options = BitmapFactory.Options()options.inScaled = false // по умолчанию true. false, если нам нужно масштабируемое изображение// загрузка из ресурсовreturn BitmapFactory.decodeResource(this, drawableRes, options)}/*** Загрузка текстуры из Bitmap и запись ее в видеопамять* @needToRecycle - нужно ли нам повторно использовать текущий Bitmap, когда пишем это GPI?*/@Throws(RuntimeException::class)fun Bitmap.toGlTexture(needToRecycle: Boolean = true, textureSlot: Int = GLES30.GL_TEXTURE0): Int {// инициализация текстурыval textureIds = IntArray(1)GLES30.glGenTextures(1, textureIds, 0) // генерируем ID для текстурыif (textureIds[0] == 0) {throw java.lang.RuntimeException("It's not possible to generate ID for texture")}   GLES30.glActiveTexture(textureSlot) // активируем слот #0 для текстурыGLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]) // привязываем текстуру по ID к активному слоту// фильтры текстурыGLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR)GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR)// записываем растровое изображение в GPUGLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, this, 0)// нам больше не нужно это растровое изображениеif (needToRecycle) {this.recycle()}// отвязываем текстуру от слотаGLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)return textureIds[0]}

extensions.kt на GitHub

Теперь мы готовы загрузить текстуру из каталога ресурсов в виде растрового изображения (bitmap), используя loadBitmapForTexture(), а затем метод QuadRender.onSurfaceCreated(). Мы привяжем текстуру к слоту текстуры OpenGL (доступны слоты от GL_TEXTURE0 до GL_TEXTURE31).

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

uTextureHandle = GLES30.glGetUniformLocation(program, FRAGMENT_SHADER_UNIFORM_TEXTURE)uTextureId = textureBitmap.toGlTexture(needToRecycle = true, GLES30.GL_TEXTURE0)

QuadRender.kt на GitHub

После этого, мы устанавливаем эту текстуру в качестве активной и видимой для фрагментного шейдера в QuadRender.onDrawFrame().

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

GLES30.glUniform1i(uTextureHandle, 0) // 0 as far as it's slot number 0// 0 если номер слота 0GLES30.glActiveTexture(GLES30.GL_TEXTURE0) // тот же слот текстуры, который мы использовали при инициализацииGLES30.glBindTexture(GLES30.GL_TEXTURE_2D, uTextureId)

QuadRender.kt на GitHub

Ссылки

Исходный код этой статьи можно найти в моем репозитории.

С библиотекой ShaderView с помощью дружественного высокоуровневого API можно познакомиться здесь.


Узнать подробнее о курсах: "Android Developer. Basic" / "Android Developer. Professional".

Также предлагаем посмотреть вебинары:

1)
Рисуем свой график котировок в Android:
- Рассмотрим основные инструменты для рисования
- Изучим возможности классов Canvas, Path, Paint
- Нарисуем кастомизируемый график котировок и добавим в него анимаций

2)
Крестики-нолики на минималках Игра на Android менее чем за 2 часа, использующийся язык Kotlin.

Подробнее..

Recovery mode Частицы в Godot 3, сглаживание маленьких частиц, и система отпечатков на шейдерах

04.09.2020 10:14:53 | Автор: admin

Использование разных частиц в Godot 3. Без использования эффектов постобработки.

Ссылки:

Запустить WebGL2 и скачать другие версии ссылка на itch.io.

Исходный код на github.

Статья разбита на разделы:

  1. Сглаживание мелких частиц, разные способы.

  2. Много шаров - частиц без потери производительности. И 3d-проекция в шейдере.

  3. Система отпечатков Screen space decals.

  4. Прочее.


1. Сглаживание частиц

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

И черное становится прозрачным.

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

Код шейдера particlelineAAbase.shader включает три версии для теста - без фильтрации, с процедурной фильтрацией используя dFd* функцию, и используя текстуру, порядок слева на право:


2. Проекция трехмерных частиц на плоскость

Использую intersection и projection логику в фрагментном шейдере.

Идея в том что - отобразить 1000 трехмерных шаров-частиц для видеокарты(GPU) стоит слишком дорого, и 1000 трехмерных шаров-частиц у меня уже нагружают GPU на 100%. Когда рендеринг 1000 кругов(не шаров) использует меньше 25% GPU.

И поворачивая плоский круг или квадрат относительно камеры можно иметь полноценный шар-частицу:

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

Исходный код шейдера шара particle_cloud_base.shader и код шейдера куба и линии.

Для шара оригинал кода взят из iquilezles.org.


3. Система отпечатков Screen space decals

Использую код шейдера из Screen-Space-Decals, мои модификации:

  1. Поворот относительно позиции, по нормали к поверхности при установке.

  2. Затухание на краях куба с dacal.

  3. Логика Material-ID для того чтобы Decals оставались только на выбранной поверхности не затрагивая другие объекты рядом.

Работает так:

Про логику Material-ID:

В Godot нет возможности записывать дополнительную информацию в основном render-pass, информацию о типе материала. Поэтому я использую отдельный Viewport в котором есть только статическая сцена:

Красный канал для material-ID синий для значения глубины(depth). Глубина(depth) используется чтобы вырезать объекты. Логика выглядит так, на скриншоте частицы и персонаж не существуют на Material-ID Viewport и вырезаются по depth:

Использование отдельного Viewport это очевидный overhead, в качестве оптимизации можно уменьшить разрешение, это можно тестировать в этой версии - в меню Debug (мышку в левый верхний угол для показа меню) и установить множитель для теста, от 0.25 до 1.


4. Прочее

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

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

В качестве оригинала такой логики я использовал этот шейдер Garden Fireworks - @P_Malin.

Шейдер для картинки загрузки - мой старый шейдер.

Анимация форм функций - моя старая демка. (старая версия так и не заработала в WebGL,по прежнему работает только в браузере на Linux)

Логика - 360 линий поворачиваются на 1 градус каждая и вращаются вокруг центральной оси, каждая линия имеет форму одной из функций. Выглядит также как в оригинальной демке:

Баги:

В ходе написания этих шейдеров нашел несколько багов в драйверах Nvidia:

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

Лицензия и используемые материалы:

  1. Все 3d модели взяты со sketchfab и имеют CC-non comercial лицензию.

  2. Музыка взята с сайта patrickdearteaga.com

  3. Весь мой код и все шейдеры имеют лицензию MIT license.

Ссылка на список используемых ресурсов.

Спасибо за чтение этой статьи.

Подробнее..

Реализация эффекта газетной фотографии на примере Quake

11.11.2020 22:08:58 | Автор: admin


Около двух лет назад вышла игра Return of the Obra Dinn за авторством Лукаса Поупа. В ней была весьма интересная стилизация графики трёхмерного мира под графику старых монохромных компьютеров.

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

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


Основа эксперимента


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

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

Поиск нужного паттерна


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

Результат выглядел любопытно, но шумновато:


Я добавил mip-текстурирование для матрицы Баера, результат улучшился, но всё равно, картинка оставалась весьма шумной:


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

Сначала я попробовал штрихи, чем цвет темнее, тем больше штрихов:


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

Я попробовал просто случайный шум:


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

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

Паттерн кругов


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

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


Этот паттерн мне навеяли старые чёрно-белые газеты, с тем отличием, что там были в основном чёрные круги на белом фоне, у меня же наоборот (почему так, см. ниже).

Паттерн мы составили, дальше встаёт вопрос, а как его применить? Применяется он весьма просто. Набор паттернов от самых тёмных (маленькие круги) до самых светлых (большие круги) объединяется в 3d-текстуру (или массив 2d текстур). Данная текстура передаётся во фрагментный шейдер, который осуществляет рисование геометрии с освещением. В шейдере происходят обычные вычисления как если бы описанных эффект не применялся делается выборка из текстуры и светокарты, высчитывается итоговый цвет. Далее определённым способом вычисляется яркость получившегося цвета и приводится в диапазон [0; 1]. Далее наступает самая интересная часть.

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


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


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


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


Теперь при приближении к текстуре круги уменьшаются а между ними появляются новые круги. Ещё можно повернуть паттерн на 45 градусов, так, кажется, эффект лучше смотрится:


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


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


Применим тонирование итогового изображения:


Окончательно откалибруем яркость:


Для того, чтобы геометрия вдалеке лучше выделялась, добавим обводку рёбер:


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

О рассчёте яркости


У меня получилась следующая формула:
brightness = clamp( 0.0, 1.0, 1.5 * mix( max( max( c.r, c.g ), c.b ), dot( c.rgb, vec3( 0.299, 0.587, 0.114 ) ), 0.5 ) );

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

О подготовке текстуры кругов


Сами круги не должны быть большими, 8x8 вполне достаточно. Главное, рисовать их сглаженными. Размер круга надо вычислить исходя из нужной яркости. До радиуса 1/2 размера ячейки этот размер пропорционален корню от яркости. Круги с радиусом 1/2 дают яркость в Pi / 4 ~ 0.785.

При радиусе больше 1/2 круги начинают пересекаться и формула вычисления их яркости становится более сложной. Я не стал с нею заморачиваться и дальше сделал просто линейную зависимость оставшегося радиуса от яркости. Эта линейная зависимость не вполне точна, но удовлетворительна.

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

Немного технической информации


Текстура паттерна у меня вышла размером 256x256 x 16 уровней яркости + мип-уровни. При использовании одноканального 8-битного формата для её хранения требуется примерно 1.34 мегабайта памяти.

Вычислительная сложность эффекта не очень высока. Требуется дополнительная выборка во фрагментном шейдере из 3d текстуры (или 2 выборки из массива 2d текстур). Также требуется немного простых вычислений.

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

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

Итоговый результат


Ещё немного снимков
















Также я сделал видео, но видеокодеки Youtube сходят с ума от высокочастотных деталей, поэтому качество видео получилось весьма плохое.




Возможные улучшения


  • Для дисплеев высокого разрешения может быть целесообразным сделать текстуру почётче с кругами 16x16 пикселей, вместо 8x8.
  • Можно несколько изменить форму точек. Например, делать плавный переход от белых точек на чёрном фоне к чёрным точкам на белом фоне.
  • Можно попытаться реализовать цветной узор. Это потребует разложение цвета на 4 типографских краски, 4 выборки (вместо одной) из текстуры паттернна, каждая со своим поворотом текстурных координат, смешивания получившихся цветов особым образом.
  • Как я уже описал выше, можно добиться других эффектов, просто поменяв алгоритм генерации текстуры паттерна, не меняя при этом шейдеров.


Заключение


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

Ссылки


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

Парящие Острова настраиваем стилизованные шейдера с помощью HDRP в Unity

08.05.2021 22:04:46 | Автор: admin

Автор статьи Maciej Hernik и главный редактор портала 80.lv Kirill Tokarev любезно позволили нам сделать этот перевод.

Maciej Hernik обсудил с нами детали его стилизованной сцены Парящие Острова: шейдеры для травы, деревьев и воды, Volume Overrides, текстурирование асcетов и многое другое.

Вступление

Всем привет! Меня зовут Maciej Hernik и я самоучка, художник по окружению (Level Artist) из Польшы. Сколько я себя помню, я всегда обожал игры. Мое знакомство с видеоиграми произошло, когда я увидел несколько на PS1. В то время я был ребенком, у меня была мечта, что однажды я буду делать свои собственные игры, хотя я еще и понятия не имел как их делать. Я стал старше, узнал что такое 3D арт и нашел это очень интересным и увлекательным занятием. Моя страсть к игровому арту дала мне возможность получить свою первую работу и в этот момент я понял, что хочу зарабатывать на жизнь созданием игрового арта и самих игр.

Парящие Острова: идея

В начале, эта работа предполагалась, как бессрочный проект вроде площадки для экспериментов с HDRP в Unity, это бы помогло мне чуть больше ознакомиться с инструментарием HDRP. Однако, когда я сделал первые итерации шейдеров растительности и показал их своим друзьям, они вдохновили меня сделать красочную сцену с использованием этих шейдеров. Тогда то я и решил сделать эту художественную работу в стиле волшебной сказки, вдохновленный игрой The Legend of Zelda. И в этой статье я хочу разобрать некоторые компоненты из этой работы.

Начало проекта

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

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

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

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

Создание травы

Шейдер травы был одним из первых созданных для этого проекта. Однако он прошел через множество итераций, чтобы достичь наиболее подходящего эффекта. Я выбрал более процедурный подход, так как посчитал, что это будет наиболее сложный и интересный путь для создания травы. Для создания шейдера я использовал Shader Graph в Unity версии 2020.1.0f1.

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

  • Primary color + Shadow color (Главный цвет + Цвет тени)

  • Additional color (Дополнительный цвет)

  • Ground color ( Цвет земли)

  • Highlights (Блики)

Primary color и Shadow color разделены на две части. Одна часть это просто цвет травы, а вторая часть отвечает за нанесение поверх другого цвета. Это достигается за счет проекции текстуры на траву из вида сверху в мировых координатах (World Position). Additional color использовался, чтобы достичь большего разнообразия травы, потому что я чувствовал, что двух цветов будет недостаточно, особенно если смотреть сверху. Ground color нужен, чтобы достичь деликатной, почти незаметной имитации окружающего затенения (Ambient Occlusion) снизу травы, чтобы немного разбить элементы. В конечном итоге, я уменьшил его, потому что он создавал слишком сильный контраст между пучками травы и это не подходило для стилизации, по-моему мнению. Именно поэтому, этот эффект окружающего затенения (Ambient Occlusion) от Ground color, очень слабо использован в этой сцене. Последний слой создает блики (Highlights) в верхней части травы, зависящие от скорости перемещения текстуры маски.

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

Деревья

Я создал отдельный шейдер для деревьев и другой растительности, так как он немного отличается от шейдера травы. В этот раз использовался освещаемый шейдер как основу (Lit Master Shader). Благодаря этому я очень легко мог получить информацию от направления света, а также в HDRP освещаемый шейдер (Lit Master Shader) дает возможность управлять полупрозрачностью (Translucency) объекта.

Модель дерева состоит из простых плоскостей (Planes), но с измененными нормалями, чтобы достичь более мягкий вид и получить лучший контроль над распределением света по дереву. Я добился этого в Blender благодаря модификатору передачи данных (Data Transfer Modifier), который позволил мне перенести нормали c другого меша на плоскости (Planes) из которых состоит дерево.

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

Я также реализовал возможность регулировать цвет в зависимости от высоты объекта, по локальному значению Y. Это позволило мне создать симпатичный градиент сверху вниз, что также помогло сделать более мягкий переход от земли к дереву. В итоге, я решил добавить деликатное окружающее затенение (Ambient Occlusion) между листьями, чтобы немного разделить их.

Эффекты воды

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

Водопад на самом деле представляет из себя микс двух составляющих:

  • Вода с шейдером воды

  • Частицы пены и пузырьков

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

Я добавил немного частиц для симуляции пены и пузырьков, используя систему частиц Unity c кастомным мешем в виде сферы. Чтобы добиться финального вида, я поиграл с такими настройками как эмиссия, форма системы частиц и размер частиц в течение их времени жизни (Size Over Lifetime).

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

Ассеты не растительного происхождения

Аcсеты, такие как камни, мост и Монолит я сделал стандартным образом - смоделировал их в Blender, заскульптил высокополигональные модели (High Poly) в ZBrush, и запек их на низкополигональные меши (Low Poly) в Substance Painter.

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

Фокус в том, чтобы использовать Blur Slope фильтр на слое Baked Lighting в Substance Painter, который я кладу поверх всех остальных слоев. Это позволило мне достичь стилизованного неоднородного эффекта.

Очень важно знать, что слой Baked Lighting добавляет тени в текстуру, основываясь на освещении именно от окружения в самом Substance Painter. Из-за этого, я выбрал окружение, которое не такое направленное с точки зрения освещения.

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

Настройка Volume

Последняя часть проекта, о которой я хотел поговорить, это компонент Volume в Unity HDRP, он позволил мне отрегулировать освещение и пост-обработку (Post-Processing). В Volume я добавил несколько Overrides, что помогло мне достичь разных эффектов.

В виде Overrides были добавлены Visual Environment, HDRI Sky и Indirect Lighting Controller, это обеспечило мне немного окружающего освещения (Ambient Light) от неба. При использовании Indirect Lighting Controller я мог регулировать интенсивность окружающего освещения (Ambient Light), что было довольно удобно для меня.

Еще одна полезная опция, которую я действительно люблю использовать внутри Unity HDRP, это туман (Fog) и особенно объемный туман (Volumetric Fog). Я обнаружил, что лучший способ использовать его - это настроить пару компонентов Density Volume в сцене. Density Volume - это компонент, который позволяет вам вручную регулировать область, где будет отображаться туман и на сколько сильным он будет в этой области.

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

Заключение

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

Если у вас есть какие-то вопросы об этой сцене или о творчестве в целом, то не стесняйтесь писать мне наArtStation.

Спасибо вам за прочтение! Пока!
Maciej Hernik, Level Artist

С оригинальной статьей можно ознакомиться на 80.lvздесь.

Перевод подготовлен при поддержке проектаAlmost There.

Подробнее..

Перевод Парящие Острова настраиваем стилизованные шейдера с помощью HDRP в Unity

09.05.2021 10:20:54 | Автор: admin

Автор статьи Maciej Hernik и главный редактор портала 80.lv Kirill Tokarev любезно позволили нам сделать этот перевод.

Maciej Hernik обсудил с нами детали его стилизованной сцены Парящие Острова: шейдеры для травы, деревьев и воды, Volume Overrides, текстурирование асcетов и многое другое.

Вступление

Всем привет! Меня зовут Maciej Hernik и я самоучка, художник по окружению (Level Artist) из Польши. Сколько я себя помню, я всегда обожал игры. Мое знакомство с видеоиграми произошло, когда я увидел несколько на PS1. В то время я был ребенком, у меня была мечта, что однажды я буду делать свои собственные игры, хотя я еще и понятия не имел как их делать. Я стал старше, узнал что такое 3D арт и нашел это очень интересным и увлекательным занятием. Моя страсть к игровому арту дала мне возможность получить свою первую работу и в этот момент я понял, что хочу зарабатывать на жизнь созданием игрового арта и самих игр.

Парящие Острова: идея

В начале, эта работа предполагалась, как бессрочный проект вроде площадки для экспериментов с HDRP в Unity, это бы помогло мне чуть больше ознакомиться с инструментарием HDRP. Однако, когда я сделал первые итерации шейдеров растительности и показал их своим друзьям, они вдохновили меня сделать красочную сцену с использованием этих шейдеров. Тогда то я и решил сделать эту художественную работу в стиле волшебной сказки, вдохновленный игрой The Legend of Zelda. И в этой статье я хочу разобрать некоторые компоненты из этой работы.

Начало проекта

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

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

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

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

Создание травы

Шейдер травы был одним из первых созданных для этого проекта. Однако он прошел через множество итераций, чтобы достичь наиболее подходящего эффекта. Я выбрал более процедурный подход, так как посчитал, что это будет наиболее сложный и интересный путь для создания травы. Для создания шейдера я использовал Shader Graph в Unity версии 2020.1.0f1.

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

  • Primary color + Shadow color (Главный цвет + Цвет тени)

  • Additional color (Дополнительный цвет)

  • Ground color ( Цвет земли)

  • Highlights (Блики)

Primary color и Shadow color разделены на две части. Одна часть это просто цвет травы, а вторая часть отвечает за нанесение поверх другого цвета. Это достигается за счет проекции текстуры на траву из вида сверху в мировых координатах (World Position). Additional color использовался, чтобы достичь большего разнообразия травы, потому что я чувствовал, что двух цветов будет недостаточно, особенно если смотреть сверху. Ground color нужен, чтобы достичь деликатной, почти незаметной имитации окружающего затенения (Ambient Occlusion) снизу травы, чтобы немного разбить элементы. В конечном итоге, я уменьшил его, потому что он создавал слишком сильный контраст между пучками травы и это не подходило для стилизации, по-моему мнению. Именно поэтому, этот эффект окружающего затенения (Ambient Occlusion) от Ground color, очень слабо использован в этой сцене. Последний слой создает блики (Highlights) в верхней части травы, зависящие от скорости перемещения текстуры маски.

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

Деревья

Я создал отдельный шейдер для деревьев и другой растительности, так как он немного отличается от шейдера травы. В этот раз использовался освещаемый шейдер как основу (Lit Master Shader). Благодаря этому я очень легко мог получить информацию от направления света, а также в HDRP освещаемый шейдер (Lit Master Shader) дает возможность управлять полупрозрачностью (Translucency) объекта.

Модель дерева состоит из простых плоскостей (Planes), но с измененными нормалями, чтобы достичь более мягкий вид и получить лучший контроль над распределением света по дереву. Я добился этого в Blender благодаря модификатору передачи данных (Data Transfer Modifier), который позволил мне перенести нормали c другого меша на плоскости (Planes) из которых состоит дерево.

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

Я также реализовал возможность регулировать цвет в зависимости от высоты объекта, по локальному значению Y. Это позволило мне создать симпатичный градиент сверху вниз, что также помогло сделать более мягкий переход от земли к дереву. В итоге, я решил добавить деликатное окружающее затенение (Ambient Occlusion) между листьями, чтобы немного разделить их.

Эффекты воды

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

Водопад на самом деле представляет из себя микс двух составляющих:

  • Вода с шейдером воды

  • Частицы пены и пузырьков

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

Я добавил немного частиц для симуляции пены и пузырьков, используя систему частиц Unity c кастомным мешем в виде сферы. Чтобы добиться финального вида, я поиграл с такими настройками как эмиссия, форма системы частиц и размер частиц в течение их времени жизни (Size Over Lifetime).

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

Ассеты не растительного происхождения

Аcсеты, такие как камни, мост и Монолит я сделал стандартным образом - смоделировал их в Blender, заскульптил высокополигональные модели (High Poly) в ZBrush, и запек их на низкополигональные меши (Low Poly) в Substance Painter.

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

Фокус в том, чтобы использовать Blur Slope фильтр на слое Baked Lighting в Substance Painter, который я кладу поверх всех остальных слоев. Это позволило мне достичь стилизованного неоднородного эффекта.

Очень важно знать, что слой Baked Lighting добавляет тени в текстуру, основываясь на освещении именно от окружения в самом Substance Painter. Из-за этого, я выбрал окружение, которое не такое направленное с точки зрения освещения.

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

Настройка Volume

Последняя часть проекта, о которой я хотел поговорить, это компонент Volume в Unity HDRP, он позволил мне отрегулировать освещение и пост-обработку (Post-Processing). В Volume я добавил несколько Overrides, что помогло мне достичь разных эффектов.

В виде Overrides были добавлены Visual Environment, HDRI Sky и Indirect Lighting Controller, это обеспечило мне немного окружающего освещения (Ambient Light) от неба. При использовании Indirect Lighting Controller я мог регулировать интенсивность окружающего освещения (Ambient Light), что было довольно удобно для меня.

Еще одна полезная опция, которую я действительно люблю использовать внутри Unity HDRP, это туман (Fog) и особенно объемный туман (Volumetric Fog). Я обнаружил, что лучший способ использовать его - это настроить пару компонентов Density Volume в сцене. Density Volume - это компонент, который позволяет вам вручную регулировать область, где будет отображаться туман и на сколько сильным он будет в этой области.

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

Заключение

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

Если у вас есть какие-то вопросы об этой сцене или о творчестве в целом, то не стесняйтесь писать мне наArtStation.

Спасибо вам за прочтение! Пока!
Maciej Hernik, Level Artist

С оригинальной статьей можно ознакомиться на 80.lvздесь.

Перевод подготовлен при поддержке проектаAlmost There.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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