Nabla icon indicating copy to clipboard operation
Nabla copied to clipboard

Text rendering plan

Open deprilula28 opened this issue 3 years ago • 2 comments

Description

Plan for the MSDF text rendering system

Solution proposal

  • Choice between MSDF & Vector Textures (Preference for MSDF; Should we want to change later, we need to change data structures for the glyphs & pixel shader that calculates coverage)

  • Text rendering CPU side:

    • The text string is not GPU generated, so the glyph ID (index into a table of, texture atlas offsets [there are multiple due to LoD] and pixel size), local offset (kerning), and bounding box of whole string can be pre-computed on the CPU for each string
    • Pool the geometry (vertex data) for all text using SubAllocatedDataBuffer
    • Pool the strings (offset into geometry, aabb, transform matrix etc.) using CPropertyPool
    • Expansion compute shader: Given a list of visible text strings (other system will do frustum culling), perform prefix sum over glyph counts in string with CScanner (the indirect variant), and have the last invocation set up a DrawIndirect struct to render all glyphs.
    • Use DrawIndirect to draw all text and pass the glyph data from a UTB (no HW vertex attributes) via custom indexing scheme: globalGlyphID = gl_VertexIndex >> 2.
    • Add an option for rendering without an index buffer as well (we can live with 50% more vx shader invocations) then the custom indexing scheme is: globalGlyphID = gl_VertexIndex / 6.
    • Use upper_bound(stringLengthPrefixSum,stringLengthPrefixSum+stringCount,globalGlyphID)-1P to find the localGlyphID within the string being processed, as well as the visible stringID. This will enable getting all string and glyph properties and working out the vertices of the string, as well as passing the texture offset data to frag shader.

Vertex Shader pseudocode:

/* Treat as if it's
struct GlyphInString
{
  uint16_t offsetX;
  uint16_t offsetY;
  uint8_t originalGlyphResolution[2];
  uint16_t glyphTableOffset;
}; */
layout() uniform usamplerBuffer nbl_glsl_ext_MSDFTextRenderer_StringGeometryPool; // rgba16_uint
layout() uniform samplerBuffer nbl_glsl_ext_MSDFTextRenderer_VisibleStringGeometryOffset
{
  uint data[];
} nbl_glsl_ext_MSDFTextRenderer_VisibleStringGeometryOffset;
layout() readonly buffer VisibleStringPrefixSum
{
  uint data[];
} nbl_glsl_ext_MSDFTextRenderer_VisibleStringPrefixSum;
layout() readonly buffer VisibleStringMVP
{
  mat4 data[];
} nbl_glsl_ext_MSDFTextRenderer_VisibleStringMVP;

NBL_GLSL_DECLARE_UPPER_BOUND(nbl_glsl_ext_MSDFTextRenderer_VisibleStringPrefixSum);

layout(location = 0) out vec2 uv;
layout(location = 1) out uint glyphTableOffset;

void main()
{
  const uvec2 uintUV = uvec2(gl_VertexIndex,gl_VertexIndex>>1)&0x1u;
  uv = vec2(uintUV);

  const uint globalGlyphID = gl_VertexIndex>>2;
  const uint visibleStringID = upper_bound_nbl_glsl_ext_MSDFTextRenderer_VisibleStringPrefixSum(globalGlyphID)-1u;
  const uint localGlyphID = globalGlyphID-nbl_glsl_ext_MSDFTextRenderer_VisibleStringPrefixSum.data[visibleStringID];
  const uint stringGlyphStart = nbl_glsl_ext_MSDFTextRenderer_VisibleStringGeometryOffset.data[visibleStringID];

  const uint glyphInStringIndex = stringGlyphStart+localGlyphID;
  const uvec4 glyphInStringData = texelFetch(nbl_glsl_ext_MSDFTextRenderer_StringGeometryPool, glyphInStringIndex);
  uint offsetX = glyphInStringData.r;
  uint offsetY = glyphInStringData.g;
  uvec2 originalGlyphResultion = unpackHalf2x16(glyphInStringData.b);
  glyphTableOffset= glyphInStringData.a;
  
  const vec2 pos = vec2(originalGlyphResultion * uintUV + uvec2(offsetX,offsetY));
  const mat4 mvp = nbl_glsl_ext_MSDFTextRenderer_VisibleStringMVP.data[visibleStringID]; // mvp has division by string aabb, hence why offsetX/Y are uint instead of float
  gl_Position = mvp * pos;

}

Frag shader pseudocode:

  /* Treat as if its
  struct data_t
  {
    unorm16x2 originalGlyphResolution;
    unorm16x2 mipMapOffsets[];
  };
  starting from a given DWORD offset*/
layout() uniform usamplerBuffer nbl_glsl_ext_MSDFTextRenderer_GlyphTable; // rg16_uint
layout() uniform sampler2D nbl_glsl_ext_MSDFTextRenderer_Glyphs; // rgba8

layout(location = 0) in vec2 uv;
layout(location = 1) in uint glyphDWORDOffset;

void main()
{
  const mat2 pixelFootprintInGlyph = mat2(dFdxFine(uv),dFdyFine(uv));

  const uvec2 originalResolution = texelFetch(nbl_glsl_ext_MSDFTextRenderer_GlyphTable,glyphDWORDOffset);

  const uint lod = someFunc(pixelFootprintInGlyph);

  const uvec2 offsetInAtlas = texelFetch(nbl_glsl_ext_MSDFTextRenderer_GlyphTable,glyphDWORDOffset+1+lod);

  const vec4 msdf = textureLod(nbl_glsl_ext_MSDFTextRenderer_Glyphs,(vec2(offsetInAtlas)+uv*vec2(originalResolution))/vec2(textureSize(nbl_glsl_ext_MSDFTextRenderer_Glyphs,0)),0); // no mipmaps on the glyph texture due to Virtual Texturing 
}

deprilula28 avatar Jun 29 '22 12:06 deprilula28

now time for the PR with interfaces

@devshgraphicsprogramming #371

deprilula28 avatar Jun 29 '22 14:06 deprilula28