1 पॉइंट द्वारा GN⁺ 2024-11-11 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Render pass के अंदर debug message जल्दी प्रिंट करने की स्थिति में font atlas तैयार करना बोझिल होता है, इसलिए fragment shader constants मात्र से text draw करने का तरीका उपयोगी है
  • Glyph को 8x16 pixel bitmap के रूप में दिखाया जाता है, और printable ASCII के 96 characters को 1536 bytes के uvec4 array में रखकर shader में सीधे lookup किया जाता है
  • Font data को PSF1 terminal font से 4-byte header और 512-byte non-printable glyph section को छोड़ने के बाद, अगले 96 glyphs निकालकर बनाया जा सकता है
  • Rendering में 4-character unit uint32_t और position·scale रखने वाले per-instance struct का उपयोग करके, पूरे text को single instanced draw call से draw किया जाता है
  • यह तकनीक simple debug output पर केंद्रित है, इसलिए 4-character padding, \0 fill character discard, और endianness correction जैसी सीमाएँ application और shader, दोनों में मिलकर संभालनी पड़ती हैं

Font atlas की जगह shader constants का उपयोग

  • सामान्य text rendering में उपलब्ध glyphs को font atlas में render किया जाता है, फिर उसे texture के रूप में bind करके, हर glyph के लिए उपयुक्त triangles draw किए जाते हैं
  • imgui और stb_truetype भी यही तरीका इस्तेमाल करते हैं, लेकिन तेज़ debug message output के लिए इसकी तैयारी झंझट भरी हो सकती है
  • विकल्प यह है कि font atlas के बराबर data को fragment shader के integer constants में store किया जाए
  • Integer को bitmap की तरह इस्तेमाल किया जा सकता है, इसलिए fragment के xy position को किसी bit position से map करके, वह bit on हो तो foreground color और off हो तो background color output किया जाता है

8x16 bitmap glyphs और ASCII table

  • एक byte केवल pixels की एक row को ही दिखा सकता है, इसलिए अधिक पढ़ने योग्य glyphs के लिए प्रति glyph 16 bytes का उपयोग किया जाता है
  • एक glyph इस तरह 8x16 pixel canvas बन जाता है, और GLSL का एक uvec4 ज़रूरी 16 bytes को ठीक-ठीक रख सकता है
  • Printable ASCII के 96 glyphs store करने पर कुल data 1536 bytes होता है
  • font_data[96] array में ASCII value से 0x20 घटाकर index इस्तेमाल किया जाता है
    • यह 0x20 SPACE से शुरू होने वाले printable ASCII glyphs को target करता है
    • Example code में space बचाने के लिए केवल कुछ entries ही दिखाई गई हैं
  • पूरा bitmap table Island source code में शामिल है

PSF1 font से bitmap extract करना

  • ज़रूरी bitmap encoding PSF1 format के terminal fonts से लगभग पूरी तरह मेल खाती है
  • PSF1 terminal font से data extract करने की प्रक्रिया सरल है
    • ImHex जैसे hex editor में font file खोलें
    • 4-byte header छोड़ दें
    • 512-byte non-printable glyph section छोड़ दें
    • अगले 96 glyphs, यानी 1536 bytes, को “Copy as → C Array” के रूप में export करें
  • निकाले गए char array को uint array के रूप में संपादित करके फिर uvec4 units में group किया जा सकता है
  • Raw char को वैसे ही जोड़कर uint बनाने पर endianness उलट जाती है, इसलिए sampling के समय इसे फिर से correct किया जाता है
  • इस्तेमाल किए गए pixel font का original data Scott Fial के free pixel font Tamsyn से लिया गया है

Single instanced draw call की संरचना

  • Text rendering को एक instanced draw call से संभाला जाता है
  • Draw call दो attribute streams का उपयोग करता है
    • Per-draw stream में केवल सामान्य quad draw करने के लिए ज़रूरी जानकारी होती है
    • Per-instance stream में screen पर position offset और print किया जाने वाला text होता है
  • Position offset के लिए x, y float उपयोग किए जाते हैं, और बचे हुए float space में font scale रखा जा सकता है
  • Vulkan में vertex output binding के सभी components में एक जैसी interpolation property होनी चाहिए, इसलिए vec3 और uint को एक binding में साफ़-सुथरे ढंग से मिलाना कठिन है
  • Text को 4-character units में uint32_t में pack किया जाता है
    • Base vertex attribute data type की minimum unit आमतौर पर 32-bit होती है, इसलिए 4 characters एक साथ रखे जाते हैं
    • Message length 4 से विभाज्य होनी चाहिए
    • कम पड़ने वाला हिस्सा \0 character से भरा जाता है
  • Per-instance data को word_data struct के रूप में दिखाया गया है
    • pos_and_scale[3]: xy position और scale
    • word: print किए जाने वाले चार characters
  • Application message को 4-character chunks में बाँटता है, हर chunk को uint32_t में बदलता है, फिर उसे position offset के साथ word_data array में जोड़ता जाता है
  • Rendering के समय इस array को debug text drawing pipeline के per-instance binding के रूप में bind करके, quads की संख्या के बराबर instances draw किए जाते हैं

Vertex Shader में position और characters पास करना

  • Vertex shader gl_Position, render किया जाने वाला word, और texture coordinates के बराबर values output करता है
  • gl_Position per-instance pos_and_scale data का उपयोग करके NDC coordinate system में triangle vertices को screen पर रखता है
  • Render किया जाने वाला word input attribute uint को fragment shader तक सीधे पास करता है
    • इसे interpolate होने से रोकने के लिए flat qualifier का उपयोग होता है
  • Texture coordinates को gl_VertexIndex से synthesize किया जाता है
    • 12 >> gl_VertexIndex & 1 से 0, 0, 1, 1 sequence बनती है
    • 9 >> gl_VertexIndex & 1 से 1, 0, 0, 1 sequence बनती है
    • इस संयोजन से बिना branching के (0,1), (0,0), (1,0), (1,1) uv coordinates बनते हैं
  • Vertex shader foreground color और background color भी per-instance data से लेकर fragment shader तक भेजता है

Fragment Shader में glyph sampling

  • Fragment shader को text render करने के लिए तीन जानकारियाँ चाहिए
    • Interpolated fragment uv coordinates
    • Output किया जाने वाला character data in_word
    • Glyph bitmap array font_data
  • Uv coordinates normalized float range vec2(0.f,0.f) से vec2(1.f,1.f) तक होते हैं, जबकि glyph pixel coordinates uvec2(0,0) से uvec2(7,15) तक होते हैं
  • पूरे 4-character word को चौड़ाई 32 pixels और ऊँचाई 16 pixels वाले क्षेत्र की तरह माना जाता है
    • uv.xy * vec2(8 * WORD_LEN, 16) को floor करके word pixel coordinates में quantize किया जाता है
    • Coordinate range को uvec2(0..31, 0..15) तक सीमित किया जाता है
    • word_pixel_coord.x / 8 से पता चलता है कि चार में से किस character क्षेत्र में हैं
    • word_pixel_coord.x % 8 से glyph के अंदर का x coordinate मिलता है
  • Character code को font_data के index में बदला जाता है
    • पहला glyph 0x20 SPACE है, इसलिए printable_character - 0x20 को offset की तरह उपयोग किया जाता है
    • उसी offset से uvec4 glyph bitmap लाया जाता है
  • y coordinate के लिए glyph_pixel_coord.y / 4 के जरिए uvec4 के अंदर एक खास uint चुना जाता है
    • यह uint pixels की 4 rows का data रखता है
    • क्योंकि ImHex से निकाले गए char को वैसे ही जोड़कर uint बनाया गया था, इसलिए row order उलटा हो जाता है
    • (8*(3-(glyph_pixel_coord.y)%4)) तरीके से पीछे से index करके इसे correct किया जाता है
  • x coordinate के लिए 7-glyph_pixel_coord.x से bit चुना जाता है
    • Byte का सबसे significant bit सबसे ऊँचे index पर store होता है, इसलिए बाएँ से दाएँ mapping के लिए reverse indexing चाहिए
  • अंतिम current_pixel value से mix(background_colour, foreground_colour, current_pixel) लागू करके रंग तय किया जाता है

छोटी strings और fill character का प्रबंधन

  • अगर string length 4 से विभाज्य नहीं है, तो application कम पड़ने वाला हिस्सा \0 character से भर देती है
  • Fragment shader यह जाँचता है कि output किया जाने वाला character \0 है या नहीं
  • \0 fill character मिलने पर background भी draw नहीं किया जाता और discard चलाया जाता है
  • इस प्रक्रिया से 4-character packing constraint बनाए रखते हुए भी छोटी strings output की जा सकती हैं

उपयोग का रूप और code की जगह

  • Island project में le::DebugPrint को call करके screen पर debug text output किया जा सकता है
  • पूरा fragment shader code github पर देखा जा सकता है
  • Code example string data पास करके "That's all, %s" रूप का message screen पर output करता है

1 टिप्पणियां

 
GN⁺ 2024-11-11
Hacker News की राय
  • अगर आप खुद करके देखना चाहते हैं, तो arithmetic को follow कर सकते हैं। ShaderToy में इसे scratch से implement करना बेहद आसान है, और अगर आपको ऐसी चीज़ें पसंद हैं तो यह शनिवार सुबह का मज़ेदार खेल भी हो सकता है
    scratch से बनाना मज़ेदार है, लेकिन शुरू करने के लिए hint चाहिए तो मैंने अभी एक example बनाया है: https://www.shadertoy.com/view/Mc3cW2
    दूसरों के बनाए कई clever text hacks भी हैं, जैसे 300 characters से कम वाला Matrix example https://www.shadertoy.com/view/llXSzj या green CRT display effect https://www.shadertoy.com/view/XtfSD8

    • जब भी मैंने immediate mode text rendering आज़माया, छोटे sizes पर text को अच्छा नहीं बना पाया। पहले ShaderToy में भी vec2(30, -30) में 30 को 300 से बदलें तो artifacts दिखते हैं
      सोचता हूँ कि इसे सही तरह handle करने की कोई trick है क्या। मेरे मामले में fragment shader के अंदर texture को multi-sample करना सबसे अच्छा चला, लेकिन फिर भी state-of-the-art जितना अच्छा नहीं था
    • मैं Unity game development करता हूँ, और इस तरफ चीज़ें सचमुच उल्टी दिशा में जाती लगती हैं। कुछ साल पहले Unity ने एक व्यक्ति द्वारा बनाया गया सबसे अच्छा font rendering tool खरीदकर default feature में शामिल कर लिया, और उसके बाद development लगभग रुक गया और competing font rendering market भी खत्म हो गया
      कुछ समय पहले मैंने native console font जैसा दिखने वाला app बनाने की कोशिश की थी, और 90% तक match करने में ही 2 घंटे से ज़्यादा tweak करना पड़ा
    • विषय से थोड़ा हटकर, लेकिन दिलचस्प example के तौर पर, HTML/CSS/JS में बना Matrix effect 1024 bytes में फिट हो जाता है: https://codegolf.stackexchange.com/a/17414
  • कल्पनाशील और hacky है, इसलिए मज़ेदार है। सच कहें तो लगभग सारी 3D rendering techniques ऐसी ही होती हैं, लेकिन अगर output किसी पुराने electronic bulletin board को recreate करने के लिए नहीं है, तो यह खास सुंदर नहीं दिखता
    और bits जोड़कर इसे बेहतर किया जा सकता है, लेकिन अच्छा दिखने से काफी पहले ही आप सारे bits set करने का आसान तरीका ढूँढने लगेंगे। आखिर में, drawing program में black-and-white pixels बनाकर texture के रूप में save करने से ज़्यादा efficient solution शायद ही मिलेगा, यानी बात फिर वहीं आ जाती है
    अगर जानना हो कि modern 3D rendering engines text draw करने के लिए आम तौर पर क्या इस्तेमाल करते हैं, तो SDF text और MSDF जैसी related techniques देखें। preprocessing step में traditional texture atlas का इस्तेमाल करके signed distance field atlas बनाने का तरीका है

    • “बात फिर वहीं आ जाती है” के संदर्भ में, अगर अभी तक नहीं देखा हो तो 1968 का paper On the Design of Display Processors देखने लायक है: http://cva.stanford.edu/classes/cs99s/papers/myer-sutherland...
      वह paper hardware के बारे में है, लेकिन software में भी reincarnation होता है
    • SDF text या MSDF जैसे तरीके अब कम-से-कम एक generation पुराने हो चुके हैं। आजकल लगभग सभी https://sluglibrary.com जैसी approach इस्तेमाल करते हैं, जिसमें shader में font Bezier curves को सीधे rasterize किया जाता है
      मैंने पहले इस concept का एक बहुत basic version बनाया था: https://www.shadertoy.com/view/sdXBDs
    • उदाहरण के लिए, जहां texture upload ठीक से काम नहीं कर रहा हो, वहाँ debug text के लिए यह काफी clever है। हालांकि original text ने sprite sheet की तुलना 16वीं सदी की manual typesetting से की है, जो cute है, लेकिन practically printing assistant को metal type set करने में एक घंटा लगता था और GPU पर sprite sheet upload करने में 10ms से भी कम लगता है, और उसके बाद यह अनंत बार configure किया जा सकता है
      इसका मतलब यह नहीं कि यह cool trick नहीं है; सच में cool trick है
  • text को mesh के रूप में render करने का विकल्प भी है। TextMeshPro एक कदम आगे जाकर arbitrary scaling handle करने के लिए signed distance fields इस्तेमाल करता है
    https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...

    • और आगे जाएँ तो GPU पर font curves को सीधे evaluate करने का तरीका भी संभव है, और scale या perspective की परवाह किए बिना high quality दे सकता है। इसे efficiently implement करना बहुत मुश्किल है, लेकिन संभव है
      example: https://sluglibrary.com
      mesh और SDF में GPU side काफी ज्यादा simple रहता है, लेकिन बहुत बड़ा zoom करने पर accuracy गिर सकती है, और mesh को बहुत छोटा shrink करने पर aliasing हो सकती है
  • बहुत बढ़िया। “traditional” texture approach के साथ performance comparison हो तो दिलचस्प होगा
    आजकल के GPU पर ऐसे simple कामों के लिए “performance कैसी है?” सवाल का जवाब अक्सर “चल जाता है” लगता है

    • “performance कैसी है?” पर मुझे जो जवाब चाहिए वह है “VSCode अब मेरे VRAM के सैकड़ों MB से लेकर कई GB तक नहीं खाता”
  • Sebastian Lague ने अलग-अलग font rendering techniques पर एक अच्छा video बनाया है
    https://youtu.be/SO83KQuuZvg

  • मैंने एक मिलती-जुलती तकनीक आज़माई थी, जिसमें पूरे font data को fragment shader के source code के अंदर डाल दिया था। इससे snprintf के ज़रिए CPU पर mapped GPU buffer में सीधे output किया जा सकता था। मुझे पता है कि यह जोखिम भरा तरीका है
    अलग-अलग characters को vertex shader से draw करने के बजाय, मैंने सिर्फ़ एक full-screen triangle draw किया और UV coordinates की जगह gl_FragCoord इस्तेमाल किया। यह सबसे efficient तरीका नहीं है, लेकिन debug feature था और व्यवहार में काफ़ी तेज़ था
    filename के उलट, इसमें NES नहीं बल्कि IBM PC ROM का font इस्तेमाल हुआ। “NES font” और दूसरे 8x8 pixel fonts वेब पर मिल जाते हैं
    https://github.com/rikusalminen/triangles/blob/nesfont/shade...

    • मेरा पसंदीदा pixel font pack यह है: https://int10h.org/oldschool-pc-fonts/
    • मुझे हाल ही में पता चला कि “NES” font 1976 के arcade game Quiz Show से आया था। यह font दूसरे black-and-white Kee/Atari games में भी इस्तेमाल हुआ था, और font data quizshow MAME ROM set में है, लेकिन किसी वजह से nibble units में तोड़ा हुआ है
      दिलचस्प बात यह है कि इस game ने questions और answers data को 8-track tape पर store किया था
  • शानदार। ऐसे text rendering algorithms अक्सर देखने को नहीं मिलते जिन्हें मैंने खुद try न किया हो। अपने startup में मैंने कई तरह के implementations किए थे, लेकिन मुझे resolution independence और anti-aliasing चाहिए था, इसलिए यह तरीका मेरे काम नहीं आता
    हो सकता है कि यह सभी Bezier curve font files पर generalize न हो। Curves को pixels में बदलना मुश्किल हो सकता है, खासकर जब glyph खुद से intersect करता हो। कुल मिलाकर standard text rendering solved-सा लगता है, और non-standard use cases आज़माना काफ़ी कठोर/मुश्किल है
    यह तरीका conceptually Will Dobbie के method जैसा लगता है, जो मुझे पसंद है। हालांकि यह कहीं ज़्यादा सरल है। दोनों raw font data लेते हैं और shader में सीधे इस्तेमाल करते हैं। फर्क यह है कि यह तरीका pixel data को array में store करता है, जबकि Will SVG path data को “vector texture” के रूप में store करते हैं
    अगर दिलचस्पी हो तो Will का शानदार demo है: https://wdobbie.com/warandpeace/

  • पहले मैंने भी ऐसा कुछ करने के बारे में सोचा था, लेकिन मेरी समझ थी कि GPU texture rendering में खास तौर पर efficient होते हैं और bit manipulation में अपेक्षाकृत धीमे। यहाँ थोड़ी memory बच भी जाए, तो भी सोचता हूँ कि क्या यह सच में atlas इस्तेमाल करने से तेज़ होगा
    शायद normal texture में bit packing करके और fragment shader से decode कराकर दोनों तरफ़ के फायदे लिए जा सकते हैं

    • वह समझ काफ़ी पुरानी है। पिछले करीब 15 सालों के GPUs में texture lookup bit operations से लगभग 100 गुना धीमा होता है
  • मैं modern computer graphics के बारे में बहुत कम जानता हूँ, इसलिए सिर्फ़ जिज्ञासा से पूछ रहा हूँ: छोटे texture को GPU पर upload करने की cost इतनी बड़ी होती है क्या? क्या पूरी string को 2D texture में render करके उस texture को दो triangles पर display नहीं किया जा सकता?

    • Cost बड़ी नहीं है। यह technique performance से ज़्यादा debugging text को स्क्रीन पर जितना आसान हो सके उतारने के लिए है। Shader में थोड़ा data जोड़ते ही text दिखने लगता है
      इसके उलट font atlas generate करने का code लिखना होगा, या कोई existing atlas ढूंढकर load करना होगा, और उसके लिए loading code भी चाहिए। या फिर पूरा message texture में draw करना होगा और message बदलने तक result को cache करना होगा
      साथ में resource management और binding भी करनी पड़ेगी, लेकिन इस तरीके में resources की ज़रूरत नहीं है। दूसरे शब्दों में, यह general text solution नहीं बल्कि screen पर debug text दिखाने की technique है
      संदर्भ के लिए, ज़्यादातर browsers और OS texture में text draw करने के तरीके से काम करते हैं। Font को dynamically texture atlas में draw करते हैं, और उस atlas के glyphs का इस्तेमाल करके app window के हिस्सों के लिए और textures बनाते हैं। Browser में texture boundaries दिखाएँ तो सभी textures दिख सकते हैं, और Rendering->Layer borders हर texture को cyan border से दिखाता है
    • आज भी GPU memory बहुत ज़्यादा waste न करना आम तौर पर अच्छा होता है। बड़े text box textures को PCI bus के ज़रिए transfer करना पड़ता है, और upload के समय या GPU resource eviction होने पर stalls आ सकते हैं
      अपेक्षाकृत धीमा CPU अगर बहुत सारे independent texture text boxes render करने लगे, तो यह जल्दी accumulate होकर budget खा सकता है
      Glyph atlas इस्तेमाल करके draw करना resource utilization के लिहाज़ से अब भी कहीं बेहतर है। Modern text rendering pipelines zoom in/out पर glyph readability बढ़ाने के लिए अक्सर SDF या encoded Bezier curves इस्तेमाल करती हैं, और यह भी memory बचाने का अच्छा तरीका है
    • N characters को cover करने वाला एक rectangle draw करके shader में glyph चुनना, हर character के लिए अलग rectangle draw करने से तेज़ होगा। कम से कम fixed-width font में तो ऐसा ही है। हालांकि screen भर सकने वाले characters की संख्या सीमित है, इसलिए असली अंतर शायद बड़ा न हो
      Upload के नज़रिए से अंत में X bytes के glyphs होते हैं और किसी न किसी तरीके से GPU memory में जाने ही हैं। Texture हो, uniform data हो या shader constants, performance में बड़ा फर्क नहीं है। बल्कि मूल लेख की तरह shader constants में डालें तो सभी constant declarations को shader compiler process करेगा, जिससे CPU-side cost अधिक हो सकती है
      GPU side पर अहम बात यह है कि glyph data पढ़ते समय कौन-सी memory hierarchy touch होती है। Texture fetches ज़्यादातर GPUs पर dedicated L1 cache इस्तेमाल करते हैं, और शायद यह सामान्य L1 cache से बड़ा होता है। Data order भी महत्वपूर्ण है। Textures आम तौर पर pixel blocks को shade करते समय cache misses से बचने के लिए Morton order के variants में store किए जाते हैं। Practical atlas-based text renderer के लिए texture इस्तेमाल करना बेहतर होगा
      Edit: मैंने सवाल गलत पढ़ लिया था। अगर तुलना GPU पर individual glyphs draw करने और CPU पर पूरा text block draw करने के बीच है, तो यह speed और space का trade-off है। Text के लिए कितनी memory इस्तेमाल करनी है, text बदलता है या नहीं, per-character effects चाहिए या नहीं—इन बातों पर जवाब बदलता है
    • Upload से पहले पूरी string render की जा सकती है, लेकिन तब मूल रूप से आप CPU rendering इस्तेमाल कर रहे होंगे, इसलिए यह GPU द्वारा वही काम करने से धीमा होगा
      साथ ही, इसे “no texture” कहा जा रहा है, लेकिन यह तरीका भी texture ही है। बस texture अलग format और अलग जगह stored है। सचमुच texture-less font rendering vector curves को on the fly evaluate करती है
    • Application पर निर्भर करता है। खासकर अगर right-to-left scripts, CJK, emoji से सामना हो सकता है, तो यही सबसे आसान तरीका है। ज़्यादातर text हर frame नहीं बदलता, इसलिए texture cache करना worth it है, और हमारे लिए यह काफी अच्छा है
  • यह कहना कि shader में bitmap स्टोर नहीं करेंगे, और फिर ठीक shader में bitmap स्टोर करने का तरीका समझाना, काफ़ी भ्रमित करने वाला है
    संक्षेप में, bitmap font को shader में embed किया गया है

    • नहीं, बात texture में bitmap स्टोर न करने की थी। यह shader code में सीधे embed करने से अलग है
      इसे runtime पर पढ़ी जाने वाली किसी अलग file में data स्टोर करने और source code में data को सीधे शामिल करने के अंतर से तुलना कर सकते हैं