Texture के बिना text render करना
(poniesandlight.co.uk)- Render pass के अंदर debug message जल्दी प्रिंट करने की स्थिति में font atlas तैयार करना बोझिल होता है, इसलिए fragment shader constants मात्र से text draw करने का तरीका उपयोगी है
- Glyph को 8x16 pixel bitmap के रूप में दिखाया जाता है, और printable ASCII के 96 characters को 1536 bytes के
uvec4array में रखकर 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,
\0fill characterdiscard, और 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 के
xyposition को किसी 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 इस्तेमाल किया जाता है- यह
0x20SPACEसे शुरू होने वाले 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 को
uintarray के रूप में संपादित करके फिरuvec4units में 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 से विभाज्य होनी चाहिए
- कम पड़ने वाला हिस्सा
\0character से भरा जाता है
- Per-instance data को
word_datastruct के रूप में दिखाया गया हैpos_and_scale[3]:xyposition और scaleword: print किए जाने वाले चार characters
- Application message को 4-character chunks में बाँटता है, हर chunk को
uint32_tमें बदलता है, फिर उसे position offset के साथword_dataarray में जोड़ता जाता है - 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_Positionper-instancepos_and_scaledata का उपयोग करके NDC coordinate system में triangle vertices को screen पर रखता है- Render किया जाने वाला word input attribute
uintको fragment shader तक सीधे पास करता है- इसे interpolate होने से रोकने के लिए
flatqualifier का उपयोग होता है
- इसे interpolate होने से रोकने के लिए
- Texture coordinates को
gl_VertexIndexसे synthesize किया जाता है12 >> gl_VertexIndex & 1से0, 0, 1, 1sequence बनती है9 >> gl_VertexIndex & 1से1, 0, 0, 1sequence बनती है- इस संयोजन से बिना 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 coordinatesuvec2(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
0x20SPACEहै, इसलिएprintable_character - 0x20को offset की तरह उपयोग किया जाता है - उसी offset से
uvec4glyph bitmap लाया जाता है
- पहला glyph
- y coordinate के लिए
glyph_pixel_coord.y / 4के जरिएuvec4के अंदर एक खासuintचुना जाता है- यह
uintpixels की 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_pixelvalue सेmix(background_colour, foreground_colour, current_pixel)लागू करके रंग तय किया जाता है
छोटी strings और fill character का प्रबंधन
- अगर string length 4 से विभाज्य नहीं है, तो application कम पड़ने वाला हिस्सा
\0character से भर देती है - Fragment shader यह जाँचता है कि output किया जाने वाला character
\0है या नहीं \0fill 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 टिप्पणियां
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
vec2(30, -30)में 30 को 300 से बदलें तो artifacts दिखते हैंसोचता हूँ कि इसे सही तरह handle करने की कोई trick है क्या। मेरे मामले में fragment shader के अंदर texture को multi-sample करना सबसे अच्छा चला, लेकिन फिर भी state-of-the-art जितना अच्छा नहीं था
कुछ समय पहले मैंने native console font जैसा दिखने वाला app बनाने की कोशिश की थी, और 90% तक match करने में ही 2 घंटे से ज़्यादा tweak करना पड़ा
कल्पनाशील और 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 बनाने का तरीका है
वह paper hardware के बारे में है, लेकिन software में भी reincarnation होता है
मैंने पहले इस concept का एक बहुत basic version बनाया था: https://www.shadertoy.com/view/sdXBDs
इसका मतलब यह नहीं कि यह cool trick नहीं है; सच में cool trick है
text को mesh के रूप में render करने का विकल्प भी है। TextMeshPro एक कदम आगे जाकर arbitrary scaling handle करने के लिए signed distance fields इस्तेमाल करता है
https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...
example: https://sluglibrary.com
mesh और SDF में GPU side काफी ज्यादा simple रहता है, लेकिन बहुत बड़ा zoom करने पर accuracy गिर सकती है, और mesh को बहुत छोटा shrink करने पर aliasing हो सकती है
बहुत बढ़िया। “traditional” texture approach के साथ performance comparison हो तो दिलचस्प होगा
आजकल के GPU पर ऐसे simple कामों के लिए “performance कैसी है?” सवाल का जवाब अक्सर “चल जाता है” लगता है
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...
दिलचस्प बात यह है कि इस 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 कराकर दोनों तरफ़ के फायदे लिए जा सकते हैं
मैं modern computer graphics के बारे में बहुत कम जानता हूँ, इसलिए सिर्फ़ जिज्ञासा से पूछ रहा हूँ: छोटे texture को GPU पर upload करने की cost इतनी बड़ी होती है क्या? क्या पूरी string को 2D texture में render करके उस texture को दो triangles पर display नहीं किया जा सकता?
इसके उलट 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 से दिखाता है
अपेक्षाकृत धीमा 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 बचाने का अच्छा तरीका है
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 चाहिए या नहीं—इन बातों पर जवाब बदलता है
साथ ही, इसे “no texture” कहा जा रहा है, लेकिन यह तरीका भी texture ही है। बस texture अलग format और अलग जगह stored है। सचमुच texture-less font rendering vector curves को on the fly evaluate करती है
यह कहना कि shader में bitmap स्टोर नहीं करेंगे, और फिर ठीक shader में bitmap स्टोर करने का तरीका समझाना, काफ़ी भ्रमित करने वाला है
संक्षेप में, bitmap font को shader में embed किया गया है
इसे runtime पर पढ़ी जाने वाली किसी अलग file में data स्टोर करने और source code में data को सीधे शामिल करने के अंतर से तुलना कर सकते हैं