D3D11 Font Rendering



  • Hi,

    ich bastel gerade an einem DX11 Glyph-Font-Renderer, aber irgendwas will da noch nicht so richtig. Bei einigen Buchstaben fehlt links und rechts etwas. Hier mal als Beispiel
    - Das ! in der Original-Map: http://imgur.com/yqem6oy
    - Das ! gerendert: http://imgur.com/IJg2fx2
    ich dachte erst, dass vielleicht die Koordinaten von bmfont falsch wären, aber ich habe das einfach mal "manuell" mit IrfanView nachvollzogen, und das Rechteck ist schon relativ großzügig um das ! herum gehalten, da sollte also nichts abgeschnitten werden*. Auch interessant ist, dass wenn ich die Textur im Shader upscale, das deutlich besser aussieht als vorher: http://imgur.com/6Out4oV Somit sieht eine 16px Font auf 32px upgescaled besser aus, als eine 32px Font. Aber ich denke irgendwie, dass das "Original" doch am besten aussehen müsste, wenn es nur ordentlich gezeichnet würde. Nur wie?

    * Ich habe auch einfach mal die ganze Textur in einem gezeichnet, und dort bekomme ich die gleichen Artefakte.

    PS: Mir ist aufgefallen, dass die ganzen Texte die Windows 8 zeichnet sich sogar aus mehreren Farben zusammensetzen. Habe ich da irgendeine Chance an eine ähnliche Textqualität zu kommen (vielleicht sogar mit glyphen?) ohne da 200 Stunden dran zu optimieren?

    Besten Dank im Voraus. 🙂

    Hier mal ein paar vielleicht relevant Codestellen, ansonsten einfach nachfragen:
    Pixel-Shader:

    float4 ps_main(ps_input input) : SV_TARGET
    {
    	return float4(color.rgb, color.a * tex.Sample(samp, input.tc).a);
    }
    

    Textur:

    D3D11_TEXTURE2D_DESC desc = {};
    desc.Width = spec.width;
    desc.Height = spec.height;
    desc.MipLevels = 1;
    desc.ArraySize = 1;
    desc.Format = native(format, spec.type, mem_mult);
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.Usage = D3D11_USAGE_IMMUTABLE;
    desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    

    Sampler:

    D3D11_SAMPLER_DESC sampler_desc = {};
    sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; //D3D11_FILTER_ANISOTROPIC;
    sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    sampler_desc.MaxAnisotropy = 0; //8; 
    sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
    sampler_desc.MinLOD = 0;
    sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
    

    Blend State:

    D3D11_BLEND_DESC blend_desc = {};
    blend_desc.AlphaToCoverageEnable = TRUE;
    blend_desc.IndependentBlendEnable = TRUE;
    blend_desc.RenderTarget[0].BlendEnable = TRUE;
    blend_desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
    blend_desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
    blend_desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
    blend_desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
    blend_desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
    blend_desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
    blend_desc.RenderTarget[0].RenderTargetWriteMask = 0xF;
    // ...
    immediate_context_->OMSetBlendState(blend_state.get(), nullptr, 0xffffffff);
    

    Swap Chain:

    DXGI_SWAP_CHAIN_DESC sd = {};
    sd.BufferCount = 1;
    sd.BufferDesc.Width = width;
    sd.BufferDesc.Height = height;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 120;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hwnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;
    

  • Mod

    wenig beschreibung die relevant ist, aber dafuer hilfreiche bilder 🙂
    meine guesses:

    1. du zeichnest mit point filtering einen texturausschnitt der groesser ist als die flaeche auf die du es zeichnest, folglich werden ab und zu texel aus der quelltextur uebersprungen?

    2. du hast clip um die raender vom font zu entfernen damit du keine schwarzen kloetze zeichnest, der clip schwellenwert ist dabei so gross, dass die dunklen pixel entfernt werden.



  • Stimmen die Texturkoordinaten? Falls ja: Könnte es was mit Blending und Gamma Korrektur zu tun haben? In was für einem Format liegen die Bilddaten vor? Was für ein Format hat die Textur und werden die Daten richtig reingeschrieben? Was für ein Format hat dein BackBuffer?

    cooky451 schrieb:

    PS: Mir ist aufgefallen, dass die ganzen Texte die Windows 8 zeichnet sich sogar aus mehreren Farben zusammensetzen. Habe ich da irgendeine Chance an eine ähnliche Textqualität zu kommen (vielleicht sogar mit glyphen?) ohne da 200 Stunden dran zu optimieren?

    Das Feature, das du wohl meinst, nennt sich ClearType. Du brauchst eben eine entsprechende Glyphentextur und passendes Blending...



  • rapso schrieb:

    1. du zeichnest mit point filtering einen texturausschnitt der groesser ist als die flaeche auf die du es zeichnest, folglich werden ab und zu texel aus der quelltextur uebersprungen?

    Sollte eigentlich nicht, wenn die Koordinaten richtig generiert werden. Ich habe gerade auch mal im Debugger beim ! zurückgerechnet, sind sowohl auf der Textur als auch auf dem Bildschirm genau 10 Pixel.

    rapso schrieb:

    2. du hast clip um die raender vom font zu entfernen damit du keine schwarzen kloetze zeichnest, der clip schwellenwert ist dabei so gross, dass die dunklen pixel entfernt werden.

    Ich glaube was du damit meinst ist sowas wie if (a < 0.01) discard; im Shader? Nein, ich nutze blending um keine Blöcke zu zeichnen. (Siehe dots Fragen :))

    dot schrieb:

    Stimmen die Texturkoordinaten?

    Sieht so aus, ja.

    dot schrieb:

    Falls ja: Könnte es was mit Blending und Gamma Korrektur zu tun haben?

    Könnte es, ja. Habe oben mal den blend state rein editiert.

    dot schrieb:

    In was für einem Format liegen die Bilddaten vor?

    DXGI_FORMAT_A8_UNORM, gelesen aus einer 512x256 .tga Datei.

    dot schrieb:

    Was für ein Format hat die Textur und werden die Daten richtig reingeschrieben?

    Ich schreibe momentan noch direkt auf den Bildschirm.

    dot schrieb:

    Was für ein Format hat dein BackBuffer?

    DXGI_FORMAT_R8G8B8A8_UNORM



  • Wenn ich das richtig seh, verwendest du einfach lineares blending. In dem Fall ist dein Ergebnis aber auch genau, was ich erwarten würde. Immerhin hat dein Grau ja auch alpha < 1 und wenn du das dann noch mit Schwarz blendest, wirds eben dunkler werden... 😉



  • Fast alles hat einen Alpha-Wert < 1. Die Textur besteht ja nur aus Alpha-Werten, was ich mache ist die angegebene Farbe (aus einem cbuffer, hier (1, 1, 1, 1)) mit dem Alpha-Wert aus der Textur zu zeichnen. Du meinst der Alpha-Wert der grauen Flächen ist so klein, dass das komplett nach Schwarz geblendet wird? Hmm.. und was könnte ich dagegen machen? Habe noch ~0 Erfahrung was blending angeht.



  • Oh, d.h. du verwendest die Textur nur für das Alpha und setzt die Farbe auf eine Konstante? Erklär uns vielleicht doch nochmal, wie genau du deine Texcoords berechnest... 😉



  • Irgendwie habe ich das Gefühl gleich eins auf den Deckel zu kriegen für irgendeinen dummen Fehler, aber dafür bin ich ja hier fürchte ich. 🤡
    Beim Zeichnen teile ich die Bildschirmkoordinaten im Shader durch die Hälfte der Bildschirmgröße. (So kann ich immer gleich viele Pixel zeichnen, auch wenn das Fenster vergrößter/kleinert wird.)

    std::vector<float> generate_coords(const std::string& s, const glyph_map& glyphs, 
    	float /* font_height */, float shift, float tex_width, float tex_height)
    {
    	std::vector<float> coords;
    
    	shift = 0.f;
    	float xbase = 0.f;
    
    	for (const auto c : s)
    	{
    		auto found = glyphs.find(static_cast<std::uint8_t>(c));
    		if (found != glyphs.end())
    		{
    			const glyph& g = found->second;
    
    			const float screen_left = xbase + g.xoffset;
    			const float screen_top = g.yoffset;
    			const float screen_right = screen_left + g.width;
    			const float screen_bottom = screen_top + g.height;
    
    			const float tex_left = (g.x + shift) / tex_width;
    			const float tex_top = g.y / tex_height;
    			const float tex_right = (g.x + g.width + shift) / tex_width;
    			const float tex_bottom = (g.y + g.height) / tex_height;
    
    			xbase += g.xadvance;
    
    			coords.push_back(screen_left);
    			coords.push_back(screen_top);
    			coords.push_back(tex_left);
    			coords.push_back(tex_top);
    
    			coords.push_back(screen_left);
    			coords.push_back(screen_bottom);
    			coords.push_back(tex_left);
    			coords.push_back(tex_bottom);
    
    			coords.push_back(screen_right);
    			coords.push_back(screen_top);
    			coords.push_back(tex_right);
    			coords.push_back(tex_top);
    
    			coords.push_back(screen_left);
    			coords.push_back(screen_bottom);
    			coords.push_back(tex_left);
    			coords.push_back(tex_bottom);
    
    			coords.push_back(screen_right);
    			coords.push_back(screen_top);
    			coords.push_back(tex_right);
    			coords.push_back(tex_top);
    
    			coords.push_back(screen_right);
    			coords.push_back(screen_bottom);
    			coords.push_back(tex_right);
    			coords.push_back(tex_bottom);
    		}
    	}
    
    	return coords;
    }
    


  • cooky451 schrieb:

    Beim Zeichnen teile ich die Bildschirmkoordinaten im Shader durch die Hälfte der Bildschirmgröße. (So kann ich immer gleich viele Pixel zeichnen, auch wenn das Fenster vergrößter/kleinert wird.)

    Du meinst die Größe des BackBuffers? Ein Fehler, der gern gemacht wird, ist auf den Unterschied zwischen den äußeren Abmessungen eines Fenster und der Größe der Client Area zu vergessen, was dann dazu führt, dass der BackBuffer in das Fenster gestaucht wird...



  • Ich fange WM_SIZE ab und resize den BackBuffer daraufhin. Das sollte recht präzise die Client-Area sein. (Ich bekomme auch genau 1600x900 beim ersten WM_SIZE, und ja, ich verwende AdjustWindowRect. ;))



  • Hm ok, klingt eigentlich gut. Schalt vielleicht einfach mal um auf Point Sampling statt bilinearem Filtering und schau, ob es dann besser aussieht...



  • Es sieht tatsächlich deutlich besser aus 👍, weil die Buchstaben jetzt nicht mehr so "verwaschen" sind. Am Ausrufezeichen fehlen aber nach wie vor die gleichen Teile, daran lag's also nicht. Bei kleineren Fonts fällt das noch mehr auf:
    Original: http://imgur.com/2Gqvvqy
    Gerendert: http://imgur.com/AUWAYwi
    Hier sieht man auch, dass teilweise oben (und unten?) Pixel abgeschnitten sind. Ich habe schon mal versucht etwas zu tricksen und einfach 1-4 Pixel auf die normalen Koordinaten zu addieren, aber das hilft nicht wirklich was interessanterweise.



  • So lang was mit bilinearem Filtering "verwaschen" aussieht, stimmt was am Pixel <-> Texel Mapping nicht. Ich mach mir das selbst immer zunutze, um zu checken, ob ich mich nicht verrechnet hab. Denn sobald Texelcenter exakt auf Pixelcenter mappen, muss bilineares Filtering praktisch aufs Bit genau exakt das gleiche Ergebnis liefern wie Point Sampling.. 😉



  • Hm, vielleicht, ich probiere das noch mal, aber wenn ich so darüber nachdenke kann es eigentlich nicht an den Koordinaten liegen, denn ich bekomme ja exakt die gleichen Artefakte wenn ich einfach die ganze Textur auf einmal zeichne. Das muss ja schon eigentlich schon irgendwas mit dem Blending zu tun haben. Sind 8 Alpha Bits vielleicht irgendwie nicht genau genug oder so?



  • Wie lädst du die Textur? Kanns sein, dass sie auf eine 2er Potenz gepadded wird oder sowas und du das nicht berücksichtigst?



  • Hm.. nicht wirklich.., es gibt zwar eine "conversion" zwischen char und uint8_t, aber das passiert hinterm memcpy, da sollte eigentlich nichts geändert werden. Komisch, komisch.



  • Ich seh zwar auf die Schnelle kein Problem, aber evtl. stimmt bei deiner Berechnung irgendwo was nicht von wegen x + width bzw. y + height ist nicht der letzte Pixel, sondern der erste Pixel nach dem letzten. Denk dir das vielleicht nochmal genau durch, auch ob Breite und Höhe der Textur bzw. des Viewport die richtige Normalisierung bringen...



  • Was uns wieder dazu führt dass die exakt identischen (!) Artefakte auftauchen wenn ich die Textur als ganzes zeichne. Hier mal Bilder:
    Original: http://imgur.com/rOM8Dl2
    Gerendert: http://imgur.com/ownboyI



  • Und dabei setzt du die Texcoords manuell direkt auf {0,1}2\in \{0, 1\}^2 und dein Quad liegt an ganzzahligen Koordinaten und hat exakt die Abmessungen der Textur!?



  • Jap, ich rendere exakt mit

    float vertices []=
    {
    	0.f, 0.f, 0.f, 0.f, 
    	width, 0.f, 1.f, 0.f, 
    	0.f, height, 0.f, 1.f, 
    	width, height, 1.f, 1.f, 
    };
    

    Edit: Aha, wenn ich das hier im Pixel Shader mache

    return float4(tex.Sample(samp, input.tc).a, 0.0, 0.0, 1.0);
    

    verschwinden die Artefakte! Es muss also irgendwie am Alpha-Blending liegen.


Anmelden zum Antworten