Войти
ПрограммированиеФорумГрафика

Обёртки над GL-ными шейдерами - какие могут быть подходы?

Страницы: 1 2 3 4 Следующая »
#0
10:09, 22 авг. 2019

Немного помозговав выработал сейчас на C++ такую обёртку над шейдерами в OpenGL:

namespace Shader
{

class Attrib
{
  GLint location = -1;
public:
  Attrib() {};
  Attrib( GLint newLocation ): location( newLocation ) {};
  Attrib( const Attrib &src ) : location( src.location ) {};
  Attrib &operator=( const Attrib &src )
  {
    location = src.location;
    return *this;
  };

  void enable()
  { glEnableVertexAttribArray( location ); };

  void disable()
  { glDisableVertexAttribArray( location ); };

  void pointer( GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer )
  {
    glVertexAttribPointer( location, size, type, normalized, stride, pointer );
  };
  void ipointer( GLint size, GLenum type, GLsizei stride, const GLvoid *pointer )
  {
    glVertexAttribIPointer( location, size, type, stride, pointer );
  };
  void instanceDivisor( GLuint divisor )
  {
    glVertexAttribDivisor( location, divisor );
  }
};

class Uniform
{
  GLint location = -1;
public:
  Uniform()
  {};

  Uniform( GLint newLocation ) : location( newLocation )
  {};

  Uniform( const Uniform &src ) : location( src.location )
  {};

  Uniform &operator=( const Uniform &src )
  {
    location = src.location;
    return *this;
  };

  // scalar float
  void setf( GLfloat v0 )
  { glUniform1f( location, v0 ); };

  void setf( GLfloat v0, GLfloat v1 )
  { glUniform2f( location, v0, v1 ); };

  void setf( GLfloat v0, GLfloat v1, GLfloat v2 )
  { glUniform3f( location, v0, v1, v2 ); };

  void setf( GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3 )
  { glUniform4f( location, v0, v1, v2, v3 ); };

  // scalar int
  void seti( GLint v0 )
  { glUniform1i( location, v0 ); };

  void seti( GLint v0, GLint v1 )
  { glUniform2i( location, v0, v1 ); };

  void seti( GLint v0, GLint v1, GLint v2 )
  { glUniform3i( location, v0, v1, v2 ); };

  void seti( GLint v0, GLint v1, GLint v2, GLint v3 )
  { glUniform4i( location, v0, v1, v2, v3 ); };

  // array float
  void arr1f( GLsizei count, const GLfloat *v )
  { glUniform1fv( location, count, v ); };

  void arr2f( GLsizei count, const GLfloat *v )
  { glUniform2fv( location, count, v ); };

  void arr3f( GLsizei count, const GLfloat *v )
  { glUniform3fv( location, count, v ); };

  void arr4f( GLsizei count, const GLfloat *v )
  { glUniform4fv( location, count, v ); };

  // array int
  void arr1i( GLsizei count, const GLint *v )
  { glUniform1iv( location, count, v ); };

  void arr2i( GLsizei count, const GLint *v )
  { glUniform2iv( location, count, v ); };

  void arr3i( GLsizei count, const GLint *v )
  { glUniform3iv( location, count, v ); };

  void arr4i( GLsizei count, const GLint *v )
  { glUniform4iv( location, count, v ); };

  // matrix
  void mat2f( GLsizei count, GLboolean transpose, const GLfloat *v )
  { glUniformMatrix2fv( location, count, transpose, v ); };

  void mat3f( GLsizei count, GLboolean transpose, const GLfloat *v )
  { glUniformMatrix3fv( location, count, transpose, v ); };

  void mat4f( GLsizei count, GLboolean transpose, const GLfloat *v )
  { glUniformMatrix4fv( location, count, transpose, v ); };
};

class Program
{
  GLuint program = 0;
  std::map< std::string, Attrib > attribs;
  std::map< std::string, Uniform > uniforms;

public:
  Program() {};
  Program( const GLchar *vertexSrc, const GLchar *fragmentSrc )
  {
    init( vertexSrc, fragmentSrc );
  };
  virtual ~Program() { free(); };

  void free();
  void init( const GLchar *vertexSrc, const GLchar *fragmentSrc );
  virtual void postInit() {};

  void use()
  { glUseProgram( program ); };

  static void unuse()
  { glUseProgram( 0 ); };

  void enableAttribs();
  void disableAttribs();
  Attrib &attrib( const std::string &name );
  Uniform &uniform( const std::string &name );
};

использование в коде предполагается такое:

// Для ускорения доступа напрямую, а не через мапы, вручную биндим поля:
struct MonoColorScaled : public Program
{
  Uniform u_bounds;
  Uniform u_scale;
  Attrib  a_offset;
  Attrib  a_color;
  Attrib  a_position;

  void postInit() override
  {
    u_bounds  = uniform( "u_bounds" );
    u_scale    = uniform( "u_scale" );
    a_offset  = attrib( "a_offset" );
    a_color    = attrib( "a_color" );
    a_position  = attrib( "a_position" );
  }
};
...
  // где то в инициализации
  prog.init( vertColorScaledSrc, fragColorScaledSrc );

  prog.a_position.enable();
  prog.a_position.pointer( 2, GL_FLOAT, GL_FALSE, 0, 0 );

  prog.a_offset.enable();
  prog.a_offset.pointer( 2, GL_FLOAT, GL_FALSE, (2 + 4) * sizeof( GLfloat ), (void *) offsetsColorsStart );
  prog.a_offset.instanceDivisor( 1 );

  prog.a_color.enable();
  prog.a_color.pointer( 4, GL_FLOAT, GL_FALSE, (2 + 4) * sizeof( GLfloat ), (void *) (offsetsColorsStart + 2 * sizeof( GLfloat )));
  prog.a_color.instanceDivisor( 1 );
...
  // где-то в отрисовке
  prog.use();
  prog.u_bounds.setf( renderer.getWidth(), renderer.getHeight() );
  prog.u_scale.setf( Ball::circleRadius, Ball::circleRadius );
...
  glDrawElementsInstanced( GL_TRIANGLES, (Ball::circleSegs - 2) * 3, GL_UNSIGNED_SHORT, 0, batch );
...
и так далее.
Не копирую код по выставлению vbo и vao и прочих рендер-стейтов, чисто только то что происходит с объектами непосредственно завязанными на шейдеры.
Можно ли как то улучшить/упростить/сделать практичнее и/или интереснее?

#1
10:12, 22 авг. 2019

=A=L=X=

Урхо знаешь ?

#2
10:23, 22 авг. 2019

innuendo
> Урхо знаешь ?

Нет.
Судя по беглому гуглу, одной статье на хабре и вот отсюда: https://discourse.urho3d.io/t/custom-shaders-example/699/3
куску кода:

rmat.shaderParameters["ObjectColor"]=Variant(myCol);//single quotes didnt work
Там довольно мощная система материалов, XML-файлов с описаниями всего от рендер-пассов до конкретных шейдеров под конкретные мультиплатформенные API и для меня сейчас сильно избыточно. Опять таки обращение по строковому ключу - то от чего я уходил всячески. Не нра.

#3
(Правка: 10:42) 10:39, 22 авг. 2019

=A=L=X=
> Опять таки обращение по строковому ключу - то от чего я уходил всячески. Не
> нра.
>

опять будет куча строк про быструю загрузку ? гибкие движки примерно так и делают

тьфу ты только что заметил :)

std::map< std::string, Uniform > uniforms;

посмотри что такое Name в UE

#4
10:46, 22 авг. 2019

=A=L=X=
А зачем ты старьё оборачиваешь? если уж и делать, то DSA... я сейчас по мере возможности, над этим же работаю... для примера ориентируюсь на это:

+ Показать

код не мой, но нек
#5
(Правка: 11:26) 11:09, 22 авг. 2019

по моему опыту, как можно больше семантики по работе с аттрибутами, юниформами и прочей лабуды вроде:

  void arr4i( GLsizei count, const GLint *v )
  { glUniform4iv( location, count, v ); };
всё это нужно гнать из кода. ссаными тряпками. руководствоваться нужно следующими принципами:
- шейдер вообще ничего не должен знать о работе с клиентской памятью и точно не должен сам этой клиентской памятью орудовать. шейдеру даётся буфер, говорится, куда его байндить и оффсет, всё. заполнением буфера юниформами отвечает вообще отдельная сущность, которая (опять же, используя рефлекшен из шейдеров) нарезает один большой буфер констант на куски для каждого пасса, каждого дроколла, итп и выдаёт оффсеты.
- как можно больше информации о шейдере нужно вытаскивать из рефлекшена. соответствие location и имени буфера, например, надо не в коде хардкодить, а вытаскивать из текста шейдера, чтобы при каждом изменении C++ код не переписывать.
- установка юниформов по одному — зашквар. юниформы надо группировать по буферам по частоте обновления: FrameBuffer, PassBuffer, MaterialBuffer, DrawCallBuffer, которые обновляются, соответственно, раз в кадр, раз пасс, раз на материал и раз на дроколл. разумеется, это нигде не захардкодено и без необходимости некоторые из них можно не использовать. пересылать юниформы по одному не только неэффективно, но и опасно, так как можно легко один из них забыть и об этом никто не узнает. при пересылке целого буфера за один вызов можно хотя бы размер сравнить. разумеется, опять же используя reflection.
- при разработке обёртки над GAPI нет смысла ориентироваться на устаревшие stateful api, так как потом использовать тот же синтаксис для нормальных stateless api не удастся. лучше ориентироваться на современный api и потом с тем же интерфейсом реализовать api для opengl. это безопаснее, удобнее и прозрачнее.
- по моему опыту, байндинг ресурсов имеет смысл делать как по shader id, так и по строковому имени. потому что иногда производительность не слишком важна, но важно читать код и делать его устойчивым к изменением layout'а, а иногда производительность критична и надо байндить по id или использовать одинаковый resource layout для разных шейдеров.

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

    renderGraph->AddPass(legit::RenderGraph::RenderPassDesc()
      .SetColorAttachments({ 
        { indexPyramid.mipImageViewProxies[0]->Id(), vk::AttachmentLoadOp::eClear, vk::ClearColorValue(std::array<int32_t, 4>{0, 0, 0, 0})} })
      .SetDepthAttachment(depthStencilProxyId, vk::AttachmentLoadOp::eClear)
      .SetStorageBuffers({
        this->sceneResources->pointData->Id()})
      .SetRenderAreaExtent(viewportSize)
      .SetProfilerInfo(legit::Colors::carrot, "PassPointRaster")
      .SetRecordFunc([this, passData, viewportSize, projMatrix, viewMatrix](legit::RenderGraph::RenderPassContext passContext)
    {
      std::vector<legit::BlendSettings> attachmentBlendSettings;
      attachmentBlendSettings.resize(passContext.GetRenderPass()->GetColorAttachmentsCount(), legit::BlendSettings::Opaque());
      auto shaderProgram = pointRasterizerShader.program.get();
      auto pipeineInfo = this->core->GetPipelineCache()->BindGraphicsPipeline(
        passContext.GetCommandBuffer(),
        passContext.GetRenderPass()->GetHandle(),
        legit::DepthSettings::DepthTest(),
        attachmentBlendSettings,
        vertexDecl,
        vk::PrimitiveTopology::ePointList,
        shaderProgram);
      {
        const legit::DescriptorSetLayoutKey *shaderDataSetInfo = shaderProgram->GetSetInfo(ShaderDataSetIndex);
        auto shaderData = passData.memoryPool->BeginSet(shaderDataSetInfo);
        {
          auto shaderBufferData = passData.memoryPool->GetUniformBufferData<PointRasterizerShader::DataBuffer>("PointRasterizerData");

          shaderBufferData->projMatrix = projMatrix;
          shaderBufferData->viewMatrix = viewMatrix;
          shaderBufferData->viewportSize = glm::vec4(viewportSize.width, viewportSize.height, 0.0f, 0.0f);
          shaderBufferData->time = 0.0f;
        }
        passData.memoryPool->EndSet();

        std::vector<legit::StorageBufferBinding> storageBufferBindings;
        auto pointDataBuffer = passContext.GetBuffer(this->sceneResources->pointData->Id());
        storageBufferBindings.push_back(shaderDataSetInfo->MakeStorageBufferBinding("PointData", pointDataBuffer));

        std::vector<legit::ImageSamplerBinding> imageSamplerBindings;
        imageSamplerBindings.push_back(shaderDataSetInfo->MakeImageSamplerBinding("brushSampler", brushImageView.get(), screenspaceSampler.get()));

        auto shaderDataSet = this->core->GetDescriptorSetCache()->GetDescriptorSet(*shaderDataSetInfo, shaderData.uniformBufferBindings, storageBufferBindings, imageSamplerBindings);

        const legit::DescriptorSetLayoutKey *drawCallSetInfo = shaderProgram->GetSetInfo(DrawCallDataSetIndex);

        int basePointIndex = 0;
        passData.scene->IterateObjects([&](glm::mat4 objectToWorld, glm::vec3 albedoColor, glm::vec3 emissiveColor, vk::Buffer vertexBuffer , vk::Buffer indexBuffer, uint32_t verticesCount, uint32_t indicesCount)
        {
          auto drawCallData = passData.memoryPool->BeginSet(drawCallSetInfo);
          {
            auto drawCallBufferData = passData.memoryPool->GetUniformBufferData<DrawCallDataBuffer>("DrawCallData");
            drawCallBufferData->modelMatrix = objectToWorld;
            drawCallBufferData->albedoColor = glm::vec4(albedoColor, 1.0f);
            drawCallBufferData->emissiveColor = glm::vec4(emissiveColor, 1.0f);
          }
          passData.memoryPool->EndSet();

          auto drawCallSet = this->core->GetDescriptorSetCache()->GetDescriptorSet(*drawCallSetInfo, drawCallData.uniformBufferBindings, {}, {});
          passContext.GetCommandBuffer().bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeineInfo.pipelineLayout, ShaderDataSetIndex,
            { shaderDataSet, drawCallSet },
            { shaderData.dynamicOffset, drawCallData.dynamicOffset });

          passContext.GetCommandBuffer().bindVertexBuffers(0, { vertexBuffer }, { 0 });
          passContext.GetCommandBuffer().draw(verticesCount, 1, 0, 0);
        });
      }
    }));
если ты уверен, что тебе понадобится только opengl, то от сущностей вроде пайплайна и дескриптор сетов можно отказаться, это немного сократит код, но на самом деле в них есть смысл.

#6
(Правка: 11:19) 11:18, 22 авг. 2019

=A=L=X=
> какие могут быть подходы?
Используй подход как в d3d с их .fx файлами. По-моему файлы эффектов для описания пасов и материалов - очень красивый подход к рендеренгу. Он очень datadriven. Меньше говна в код надо втаскивать

#7
11:19, 22 авг. 2019

ожидается появление Andrey в теме

#8
11:20, 22 авг. 2019

=A=L=X=
> Опять таки обращение по строковому ключу - то от чего я уходил всячески. Не нра.
Напиши над ним обёртку и не будет строковых ключей

#9
11:22, 22 авг. 2019

Funtik
> А зачем ты старьё оборачиваешь?
У меня совместимо должно быть с GL ES 3.0.
Код понравился, есть рациональное зерно как делаются uniform_typed чтобы как раз не задумываться о постфиксировании функций как у меня...

#10
11:25, 22 авг. 2019

Fedor1995
> Напиши над ним обёртку и не будет строковых ключей
У меня есть мапы атрибутов и юниформов автоматически создаваемые после компиляций и линковок шейдеров - динамическая природа шейдеров без этого никак.
Но я таки втаскиваю их и как обычные компил-тайм поля и делаю линковку в методе postInit шейдера ручками к сожалению. Но так хотя бы в коде более опрятно и просто пишется и нет поиска по мапам. Сам мап даже пофигу, нечасто эти стейты выставляются, но с обращением по строке возникают проблемы с контролем времени компиляции, возможности описки и т.п.
Вообще у меня игры простые должны быть графически без обилия SFX, 3D и подповерхностного рассеивания, поэтому о сложных системах материалов пока нет смысла задумываться, кроме как в очень гипотетическое будущее заглядывать.

#11
(Правка: 12:31) 12:25, 22 авг. 2019

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

ага, при буферах потом дружно запускаем renderdoc и смотрим туда попало или не туда ... как делал один мой знакомый :)

Suslik
> и раз на дроколл

можно же загнать всё в один cbuffer и потом по смещениям

#12
12:39, 22 авг. 2019

Мне на самомSuslik
> пересылать юниформы по одному не только неэффективно, но и опасно, так как
> можно легко один из них забыть и об этом никто не узнает. при пересылке целого
> буфера за один вызов можно хотя бы размер сравнить.
А это как? Или в GL 4.x что-то такое есть?

#13
12:39, 22 авг. 2019

=A=L=X=
> А это как?

я подозреваю, что речь про UBO :)

#14
12:43, 22 авг. 2019

innuendo
> я подозреваю, что речь про UBO :)

Да, оно, не ковырялся еще в этом.

Страницы: 1 2 3 4 Следующая »
ПрограммированиеФорумГрафика