Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество

Шейдерные узлы

Опубликованно: 20.05.2017, 0:45
Последняя редакция, Andry: 29.05.2017 21:59

Мотивы

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

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

Шейдерные узлы были разработаны с учетом этого и являются плодом многих длительных обсуждений в основном чате, взвешивание плюсов и минусов той или иной модели.
Сначала эту систему называли «Шейдерной инъекцией(Shader injection)». Основная идея заключалась в том, чтобы позволить пользователям вводить код в шейдеры с помощью системы замены тегов.
Наконец, мы пришли к другой концепции под названием Шейдерные узлы(Shader Nodes), на которую нас вдохновила система узлов блендера для текстур и постобработки.
Окончательный шейдер генерируется во время выполнения приложения системой, путем сборки узлов шейдера вместе.

Что такое шейдерный узел?

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

На практике это немного больше, чем это. Шейдерный узел разделяется на несколько частей:

  • Описание шейдерного узла, описывается:
    • Минимальной версия glsl, необходимая для шейдерного узла
    • Путём к файлу шейдера (содержащий шейдерный код heh)
    • Обязательным блоком документации для этого узла Shader. Как я надеюсь, многие пользователи будут создавать свои собственные узлы и предлагать их другим и получать от других для себя, этот момент имеет решающее значение.
    • Списком входов, принимаемых этим шейдером (типизированная переменная glsl необязательна или необходима для правильного запуска кода)
    • Списком выходов, созданных этим шейдером (типизированная переменная glsl, вычисленная и передаваемая кодом узла)
  • Фактический файл кода шейдера (.vert или .frag в зависимости от типа узла, как любой шейдерный файл)
  • Объявление шейдерного узла, имеет:
    • Уникальное имя (в области шейдера)
    • Описание на основе шейдерного узла
    • Необязательное условие активации (основанное на реальной системе определения)
    • Списках входных сопоставлений (что фактически будет подаваться на эти входы)
    • Списках сопоставлений вывода (что будет выведено на глобальный вывод шейдера)

Определение Shader Node

Первые ShaderNodes должны быть определены либо в отдельном файле (j3sn для jme3 shader node), либо непосредственно внедряться в блок Technique файла j3md.
Пожалуйста, обратитесь к этой документации для более подробного ознакомления со структурой файла j3md jMonkeyEngine3 Спецификация материала.

Все включено в блок ShaderNodeDefinitions. Этот блок может иметь несколько узлов (рекомендуется вместе описывать узлы, которые имеют сильные зависимости друг от друга в том же файле j3sn).
ShaderNode объявляется в блоке ShaderNodeDefinition.
Примерная структура должна выглядеть так:

ShaderNodeDefinitions{
      ShaderNodeDefinition <NodeDefName>{
            Type : <ShaderType>
            Shader <ShaderLangAndVersion> : <ShaderPath>
            [Shader <ShaderLangAndVersion> : <ShaderPath>]
            [...]
            Documentation {
                <DocContent>
            }
            Input {
                <GlslVarType> <VarName>
                [<GlslVarType> <VarName>]
                [...]
            }
            Output {
                <GlslVarType> <VarName>
                [<GlslVarType> <VarName>]
                [...]
            }
      }
      [ShaderNodeDefinition <NodeDef2Name> {
         [...]
      }]
      [...]
}

Все, что не находится между [], является обязательным.

  • ShaderNodeDefinition: блок описания. Вы можете иметь несколько описаний в одном блоке ShaderNodeDefinitions.
    • NodeDefName: имя этого описания ShaderNodeDefinition
  • Type: определить тип этого шейдерного узла
    • ShaderType: тип шейдера для этого описания. На данный момент поддерживаются только «Вершинный» и «Фрагментный».
  • Shader: версия и путь используемого кода шейдера. Обратите внимание, что у вас может быть несколько шейдеров с различной версией GLSL. Генератор выберет соответствующий в зависимости от возможностей графического процессора.
    • ShaderLangAndVersion: придерживается того же синтаксиса, что и объявление шейдера в файле j3md: GLSL <версия>, версия 100 для glsl 1.0, 130 для glsl 1.3, 150 для glsl 1.5 и.т.д. Обратите внимание, что это минимальная версия glsl, поддерживаемая этим шейдером
    • ShaderPath путь к файлу кода шейдера (относительно папки ресурсов)
  • Documentation: блок документации. Она обязательна, и я действительно рекомендую её заполнить, если вы хотите предлогать другим свои шейдерные узлы. Эта документация будет предоставлять описание при использовании SDK у пользователей, желающим добавить этот узел в свои описания материалов. Она должно содержать краткое описание узла и описание для каждого входа и выхода.
    • @input может использоваться для префикса имени входа, чтобы sdk распознал его и отформатировал соответствующим образом. Синтаксис id @input <описание>.
    • @output может использоваться для префикса имени выхода, чтобы sdk распознал его и отформатировал соответствующим образом. Синтаксис id @output <имя входа> <описание>
  • Input: входной блок, содержащий все входы этого узла. Узел может иметь 1 или несколько входов.
    • GlslVarType: допустимый тип переменной glsl, который будет использоваться в шейдере для этого входа. См. Http://www.opengl.org/wiki/GLSL_Type и «Объявление главы массива
    • VarName: имя переменной. Обратите внимание, что вы не можете иметь несколько входов с одинаковым именем.
  • Output: Выходной блок, содержащий все выходы этого узла. Узел может иметь 1 или несколько выходов.
    • GlslVarType: допустимый тип переменной glsl, который будет использоваться в шейдере для этого входа. См. Http://www.opengl.org/wiki/GLSL_Type и «Объявление главы массива
    • VarName: имя переменной. Обратите внимание, что вы не можете иметь несколько выходов с одинаковым именем.
Если вы используете одно и то же имя для входа и выхода, генератор будет рассматривать их как переменную SAME, поэтому они должны быть одного и того же типа glsl.

Пример

Вот типичное описание шейдерного узла.

ShaderNodeDefinitions{
     ShaderNodeDefinition LightMapping{
        Type: Fragment
        Shader GLSL100: Common/MatDefs/ShaderNodes/LightMapping/lightMap.frag
        Documentation {
            This Node is responsible for multiplying a light mapping contribution to a given color.
            @input texCoord the texture coordinates to use for light mapping
            @input lightMap the texture to use for light mapping
            @input color the color the lightmap color will be multiplied to
            @output color the resulting color
        }
        Input{
            vec2 texCoord
            sampler2D lightMap
            vec4 color
        }
        Output{
            vec4 color
        }
    }
}

Объявить массив

Чтобы объявить массив, вы должны указать его размер между квадратными скобками.
Постоянный размер
Размер может быть константой int.
Пример

      float myArray[10]

Это объявление массива float с 10 элементами. Любой параметр материала, сопоставленный с этим массивом, должен быть типа FloatArray, и его размер будет принят равным 10 при генерации шейдера.

Величина, определяемая параметром материала
Размер может быть динамическим и управляться параметром материала. GLSL не поддерживает не константные значения для объявления массива, поэтому этот параметр материала будет сопоставлен с описанием.
Пример

     float myArray[NumberOfElements]

Это объявление массива float с размером, зависящим от значения параметра материала NumberOfElement.
NumberOfElement ДОЛЖЕН быть объявлен в описании материала как параметр материала. Он будет сопоставляться с описанием и использоватся в шейдере.

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

Код шейдерного узла

Шейдерный код, связанный с шейдерным узлом, и похож на любой шейдерный код.
Код для вершинного шейдерного узла должен быть в .vert-файле, а код для Фрагментного шейдерного узла должен находиться в файле .frag. Он содержит декларативную часть, содержащую объявление переменной, объявление функции и.т.д. И основную часть, которая встраивается в блок void main() {}.
Переменные входа и выхода, объявленные в описании шейдерного узла, могут использоваться без объявления в коде шейдера. (Они не должны даже, или у вас будут проблемы).
Ниже приведен код шейдера LightMap.frag.

void main(){
    color *= texture2D(lightMap, texCoord);
}

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

Объявление узла шейдера

Чтобы создать шейдер, нам нужно подключить узлы шейдеров друг к другу, но также взаимодействовать со встроенными glsl входами и выходами. Шейдерные узлы объявляются внутри блока Technique. Вершинные узлы объявляются в блоке VertexShaderNodes, а Фрагментные узлы объявляются в блоке FragmentShaderNodes.
Обратите внимание, что если j3md имеет описание узлов шейдера ember (в блоке ShaderNodesDefinitions), оно должно быть объявлено перед блоками VertexShaderNodes и FragmentShaderNodes. Конечно, в этом блоке может быть несколько объявлений ShaderNode.
Вот как должно выглядеть объявление ShaderNode:

ShaderNode <ShaderNodeName>{
     Definition : <DefinitionName> [: <DefinitionPath>]
     [Condition : <ActivationCondition>]
     InputMapping{
          <InputVariableName>[.<Swizzle>] = <NameSpace>.<VarName>[.<Swizzle>] [: <MappingCondition>]
          [...]
     }
     [OutputMapping{
          <NameSpace>.<VarName>[.<Swizzle>] = <OutputVariableName>[.<Swizzle>] [: <MappingCondition>]
          [...]
     }]
}
  • ShaderNode блок шейдерных узлов
    • ShaderNodeName имя этого шейдерного узла, может быть любым, но должно быть уникальным в области шейдера.
  • Definition: ссылка на описание узла шейдера.
    • DefinitionName: имя описания, которое использует этот узел. Это описание может быть объявлено в том же j3md или в собственном файле j3sn.
    • DefinitionPath: если описание объявлено в собственном файле j3sn, вам нужно указать путь к этому файлу.
  • Condition условие, которое указывает, активен ли узел или нет.
    • ActivationCondition: условие для использования этого узла. Сегодня мы используем Defines для использования различных блоков кода, используемых в зависимости от состояния Material Parameter. Условие здесь использует ту же самую парадигму. Допустимым условием должно быть имя параметра материала или любых комбинаций с использованием логических операторов «||», «&&», «!» Или группой символов «(and»). Генератор создаст соответствующее описание, и код узла шейдера будет встроен в и инструкцию #ifdef.

Например, предположим, что у нас есть параметр материала Color и ColorMap, это условие «Color || ColorMap будет генерировать эту инструкцию:

        #if defined(COLOR) || defined(COLORMAP)
            ...
        #endif
  • InputMapping проводки входов этого узла, поступающие от выходов предыдущего узла или встроенных входов glsl.
    • InputVariableName: имя переменной для сопоставления с объявленной в описании.
    • Swizzle: Swizling для предыдущей переменной. Дополнительная информация о glsl swizzling на этой странице http://www.opengl.org/wiki/GLSL_Type.
    • NameSpace: генератор будет использовать пространство имен переменных, чтобы избежать совпадений между именами переменных. Пространство имен может иметь одно из следующих значений:
      • MatParam: следующая переменная — параметр материала, объявленный в блоке MaterialParameters параметра materialDefinition.
      • WorldParam: следующая переменная — это World Parameter, объявленный в блоке WorldParameters текущего блока technique. Параметры мира могут быть одним из объявленных в этом файле: https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/main/java/com/jme3/shader/UniformBinding.java.
      • Attr: следующая переменная является атрибутом шейдера. Она может быть объявлен в перечислении Type класса VertexBuffer https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/main/java/com/jme3/scene/VertexBuffer.java.
      • Global: переменная является глобальной переменной для шейдера. Глобальные переменные будут назначены в конце шейдера для glsl встроенных выходов: gl_Position для вершинного шейдера или одного из возможных выходов фрагментного шейдера (например, gl_FragColor). Глобальные переменные могут иметь и то, имя что вам нравится, оно будет назначено в том порядке, в котором они были найдены в объявлении, на выходе шейдера. Глобальные переменные могут быть входами шейдера. Глобальные переменные вынуждены быть vec4 и по умолчанию равны значению атрибута inPosition в вершинном шейдере, и vec4 (1.0) (непрозрачный белый цвет) в фрагментном шейдере.
      • Имя предыдущего шейдерного узла: за ним должна следовать и выходная переменная именованного шейдерного узла. Это позволяет подключать выходы от узла к входам другого.
    • VarName: имя переменной, которая будет назначена на вход. Эта переменная должна быть известна в объявленном ранее пространстве имен.
    • MappingCondition: Выполняет те же правила, что и условие активации для shaderNode, это сопоставление будет встроено в оператор #ifdef n в результате шейдера.
  • OutputMapping: этот блок является необязательным, так как сопоставление выходных данных будет выполнено в блоке входных сопоставлений следующих shaderNodes, за исключением того, что вы хотите выводить значение на Global вывод шейдера.
    • NameSpace: пространство имен для назначаемого выхода, это могут быть только «Global».
    • VarName: имя глобального выхода (может быть любым, просто имейте в виду, что 2 разных имени приводят к двум различным выходам).
    • OutputVariable: Должен быть выходным значением описания текущего узла.
    • MappingCondition: то же, что и раньше.

Пример полного описания материала и пример шейдерных узлов

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

Шейдерные узлы работают только в том случае, если в technique не указан шейдер. Если вы хотите обойти Shader Nodes, вы можете поместить в technique VertexShader и заявление FragmentShader, и шейдерные узлы будут проигнорированы.
MaterialDef Simple {
    MaterialParameters {
        Color Color
    }
    Technique {
        WorldParameters {
            WorldViewProjectionMatrix
        }
        VertexShaderNodes {
            ShaderNode CommonVert {
                Definition : CommonVert : Common/MatDefs/ShaderNodes/Common/CommonVert.j3sn
                InputMappings {
                    worldViewProjectionMatrix = WorldParam.WorldViewProjectionMatrix
                    modelPosition = Global.position.xyz
                }
                OutputMappings {
                    Global.position = projPosition
                }
            }
        }
        FragmentShaderNodes {
            ShaderNode ColorMult {
                Definition : ColorMult : Common/MatDefs/ShaderNodes/Basic/ColorMult.j3sn
                InputMappings {
                    color1 = MatParam.Color
                    color2 = Global.color
                }
                OutputMappings {
                    Global.color = outColor
                }
            }
        }
    }
}

Это определение материала имеет один метод по умолчанию с двумя объявлениями узлов.
Описание CommonVert
CommonVert — вершинный шейдерный узел, который обычно использует входные и выходные данные вершинного шейдера. Он также вычисляет положение вершины в проекционном пространстве.
Вот описание содержимого (Common/MatDefs/ShaderNodes/Common/CommonVert.j3sn):

ShaderNodesDefinitions {
    ShaderNodeDefinition CommonVert {
        Type: Vertex
        Shader GLSL100: Common/MatDefs/ShaderNodes/Common/commonVert.vert
        Documentation {
            This Node is responsible for computing vertex position in projection space.
            It also can pass texture coordinates 1 & 2, and vertexColor to the frgment shader as 
varying (or inputs for glsl >=1.3)
            @input modelPosition the vertex position in model space (usually assigned with
 Attr.inPosition or Global.position)
            @input worldViewProjectionMatrix the World View Projection Matrix transforms
 model space to projection space.
            @input texCoord1 The first texture coordinates of the vertex (usually assigned with Attr.inTexCoord)
            @input texCoord2 The second texture coordinates of the vertex (usually assigned with Attr.inTexCoord2)
            @input vertColor The color of the vertex (usually assigned with Attr.inColor)
            @output projPosition Position of the vertex in projection space.(usually assigned to Global.position)
            @output vec2 texCoord1 The first texture coordinates of the vertex (output as a varying)
            @output vec2 texCoord2 The second texture coordinates of the vertex (output as a varying)
            @output vec4 vertColor The color of the vertex (output as a varying)
        }
        Input{
            vec3 modelPosition
            mat4 worldViewProjectionMatrix
            vec2 texCoord1
            vec2 texCoord2
            vec4 vertColor
        }
        Output{
            vec4 projPosition
            vec2 texCoord1
            vec2 texCoord2
            vec4 vertColor
        }
    }
}
Обратите внимание, что texCoord1 / 2 и vertColor объявлены как как входными, так и выходными. Генератор будет использовать для них одни и те же переменные.

Ниже приведен код шейдерного узла(Common/MatDefs/ShaderNodes/Common/commonVert.vert).

void main(){
    projPosition = worldViewProjectionMatrix * vec4(modelPosition, 1.0);
}

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

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

  • WorldViewProjectionMatrix = WorldParam.WorldViewProjectionMatrix: входному параметру worldViewProjectionMatrix присваивается параметр WorldViewProjectionMatrix World, объявленный в блоке WorlParameters этой technique.
  • ModelPosition = Global.position.xyz: modelPosition (понять положение вершины в координатном пространстве модели) присваивается Global переменной положения.
Как упоминалось ранее, Global позиция инициализируется атрибутом inPosition, поэтому это эквивалентно:

modelPosition = Attr.inPosition.xyz
Также обратите внимание на swizzle переменной Global.position. ModelPosition — это vec3, а GlobalPosition — vec4, поэтому мы просто берем первые 3 компонента.

Сопоставление выходных данных CommonVert

  • Global.position = projPosition: Результат умножения worldViewProjectionMatrix и modelPosition присваивается Global позиции.
Переменная Global.position будет назначена gl_Position glsl, построенная на выходе в конце шейдера.

Описание ColorMult
ColorMult — очень простой Шейдерный Узел, который принимает два цвета в качестве входных данных и умножает их. Вот описание содержимого (Common/MatDefs/ShaderNodes/Basic/ColorMult.j3sn):

ShaderNodeDefinitions{
    ShaderNodeDefinition ColorMult {
        Type: Fragment
        Shader GLSL100: Common/MatDefs/ShaderNodes/Basic/colorMult.frag
        Documentation{
            Multiplies two colors
            @input color1 the first color
            @input color2 the second color
            @output outColor the resulting color
        }
        Input {
            vec4 color1
            vec4 color2
        }
        Output {
            vec4 outColor
        }
    }
}

Ниже приведен код шейдерного узла (Common/MatDefs/ShaderNodes/Basic/colorMult.frag).

void main(){
    outColor = color1 * color2;
}

Сопоставление входных данных ColorMult
Все входы сопоставляются здесь:

  • Color1 = MatParam.Color: Первый цвет присваивается параметру Color Material, объявленному в блоке MaterialParameter описания материала.
  • Color2 = Global.color: второй цвет присваивается переменной Global цвета. По умолчанию используется vec4 (1.0) (непрозрачный белый).
В очень сложном материале def эта переменная могла уже быть назначена с выходом предыдущего шейдерного узла.

Сопоставление цветов ColorMult

  • Global.color = outColor: результирующий цвет присваивается Global переменной цвета.
Обратите внимание, что переменная Global.color будет назначена gl_FragColor (glsl <1.5) или объявлена как глобальный вывод шейдера (glsl> = 1.5).
Также обратите внимание, что в случае объявления нескольких глобальных переменных генератор присваивает им gl_FragData [i] (glsl <1.5) i - порядок, который была найдена в описании материала. Для glsl> = 1.5 достоверное будет просто объявлено как вывод шейдера в том порядке, в котором они были найдены в объявлении.

Сгенерированный код шейдера

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

Вершинный Шейдер (glsl 1.0)

uniform mat4 g_WorldViewProjectionMatrix;

attribute vec4 inPosition;

void main(){
        vec4 Global_position = inPosition;

        //CommonVert : Begin
        vec3 CommonVert_modelPosition = Global_position.xyz;
        vec4 CommonVert_projPosition;
        vec2 CommonVert_texCoord1;
        vec2 CommonVert_texCoord2;
        vec4 CommonVert_vertColor;

        CommonVert_projPosition = g_WorldViewProjectionMatrix * vec4(CommonVert_modelPosition, 1.0);
        Global_position = CommonVert_projPosition;
        //CommonVert : End

        gl_Position = Global_position;
}

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

Как вы можете видеть, texCoord1 / 2 и vertColor объявлены, но никогда не используются. Это потому, что генератор не знает об этом. По умолчанию он объявит все входы в случае, если они используются в коде shaderNode. Обратите внимание, что большинство glsl-компиляторов оптимизирует это при компиляции шейдера на GPU.

Фрагментный шейдер (glsl 1.0)

uniform vec4 m_Color;

void main(){
        vec4 Global_color = vec4(1.0);

        //ColorMult : Begin
        vec4 ColorMult_color2 = Global_color;
        vec4 ColorMult_outColor;

        ColorMult_outColor = m_Color * ColorMult_color2;
        Global_color = ColorMult_outColor;
        //ColorMult : End

        gl_FragColor = Global_color;
}

То же, что для вершинного шейдера. Обратите внимание, что color1 не объявлен, потому что он напрямую заменяется параметром material.

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

Дополнительные пояснения и проектные решения см. В спецификации https://docs.google.com/document/d/1S6xO3d1TBz0xcKe_MPTqY9V-QI59AKdg1OGy3U-HeVY/edit?usp=sharing.

Спасибо за храбрых, которые прошли через все это чтение. Я не собираюсь предлагать вам приз в обмен на пароль, потому что у нас кончились ремни JME …


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добавить комментарий

jMonkeyEngine.ru © 2017. Все права сохранены.