Instantly share code, notes, and snippets.
Save d7samurai/9f17966ba6130a75d1bfb0f1894ed377 to your computer and use it in GitHub Desktop.
A minimal Direct3D 11 implementation of "antialiased point sampling", useful for smooth fractional movement and non-integer scaling of pixel art AKA "fat pixel" aesthetics.
Also view below side-by-side point sampling comparison onYouTube (video is zoomed in to counter implicit downsampling & compression artifacts and make aa effect more apparent) or check out the originalShadertoy.
The actual sampler is set to bilinear filtering (the default D3D11 sampler state) in order to utilize single texture-read hardware interpolation, then emulating point sampling in the shader and applying AA at the fat pixel boundaries. Use withpremultiplied alpha textures* and keep a one pixel transparent border around each sprite/tile.
This is the algorithm (seeshaders.hlsl for context):
float2 pix =floor(p.tex) +min(frac(p.tex) /fwidth(p.tex),1) -0.5;
Note that the D3D setup herecuts a lot of corners to keep it small relative to the pixel shader containing the algorithm. For a more conventional D3D11 setup reference, see the originalMinimal D3D11
*I recently made a small (~17 KB) command line utility for this, calledtexprep.exe, available for downloadat the bottom of this Gist (note: the executable it is unsigned and so might be - falsely! - flagged as malware by e.g. Windows Defender).
It reads most common image formats, converts to 32-bit (ARGB) and saves it out to either-png,-bmp or-bin (a raw sequence ofB, G, R, A, ... bytes that can be imported into your application without the need for decoding).New! in version 1.1.x is the option to export to-txt, which creates a text file with the image data encoded as anint array[].
You can specify multiple input files. An output format will apply to subsequent files until another is specified. Default is-png.
Similarly,-pm1 turns premultiplied alpha ON and-pm0 turns it OFF. Premultiplied alpha processing is OFF by default.
Examples:
C:\>texprep -bmp -pm1 mario.png texprep : reading 'mario.png' (640x640) .. premultiplying alpha .. writing 'mario_pm1.bmp' .. done.C:\>texprep -bin onedrive.ico texprep : reading 'onedrive.ico' (40x40) .. writing 'onedrive_40x40_pm0.bin' .. done.C:\>texprep -bin cursor.cur -pm1 red50.png -png -pm0 splash.tif tiger.jpg texprep : reading 'cursor.cur' (256x170) .. writing 'cursor_256x170_pm0.bin' .. done. texprep : reading 'red50.png' (129x128) .. premultiplying alpha .. writing 'red50_129x128_pm1.bin' .. done. texprep : reading 'splash.tif' (3000x700) .. writing 'splash_pm0.png' .. done. texprep : reading 'tiger.jpg' (1920x1280) .. writing 'tiger_pm0.png' .. done.C:\>texprep -txt adventurer.gif texprep : reading 'adventurer.gif' (898x505) .. writing 'adventurer_pm0.txt' .. done.The latter will produce a text file that looks something like this:
// adventurer.gif#defineTEXTURE_WIDTH 898#defineTEXTURE_HEIGHT 505inttexture[]={0xff2c2c2c,0xff2c2c2c,0xff2c2c2c,0xff2c2c2c,0xff2c2c2c,0xff2c2c2c, ... ...};
Example use case here:Minimal D3D11 sprite renderer
| #pragma comment(lib, "user32") | |
| #pragma comment(lib, "d3d11") | |
| #pragma comment(lib, "d3dcompiler") | |
| #include<windows.h> | |
| #include<d3d11.h> | |
| #include<d3dcompiler.h> | |
| unsignedlonglong skullTexture[] =// 17 x 25 pixels, 1 byte per pixel | |
| { | |
| 0x0000000000000000,0x0000000000000000,0xdd00000000000000,0x00000000b2e0dede,0xe1dc000000000000,0x00afaeaeaec8c8c8, | |
| 0xc7e3000000000000,0x89b09999c3ddddc3,0xc7dd0000000000ae,0x9b99afb0c6dbddc6,0xc400000000af899b,0xb0aeaec6c6dedcc6, | |
| 0xdd0000008a9c99af,0xadadaec4dedcdcc4,0x0000599b9aaeafaf,0xadadc6c6ddddc6c6,0x005c9a9aafafadae,0xadacc6c3e0ddc600, | |
| 0x5c9d5ab0b0aeadad,0xaddedcdcc6ae0000,0x9f5ac4c4c7c6adad,0x895e89ae5d00005c,0x5b56578dc6aeaeb0,0x18185c3300005a5a, | |
| 0x1818198cae8c1716,0x195a330000305b31,0x171617af5a331718,0x8ec60000315a5b30,0x165b8b8a58321718,0xe000005b8c5a3132, | |
| 0x8a17dd8d5c188db1,0x000089ad8a5a1658,0x3219da888eaf8b5b,0x00323034898b8d5b,0x16aeae1719590000,0x00301717af8d3089, | |
| 0xad5a32dc00000000,0x005a1959ad8c8bb0,0x8d198a0000000000,0x58318b17e217dc5a,0x8a8a000000000000,0x5b3230185a5b2f18, | |
| 0xae00000000000033,0xb02fdc30de18ddaf,0x000000000000335a,0xb0e1aeb1afafae00,0x0000000000005daf,0xadafb1afb0000000, | |
| 0x0000000000005b5c,0x898b8b0000000000,0x0000000000005a5c,0x0000000000000000,0x0000000000000000,0x0000000000000000, | |
| }; | |
| int WINAPIWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,int nShowCmd) | |
| { | |
| WNDCLASSA wndClass = {0, DefWindowProcA,0,0,0,0,0,0,0,"d7" }; | |
| RegisterClassA(&wndClass); | |
| HWND window =CreateWindowExA(0,"d7",0,0x91000000,0,0,0,0,0,0,0,0); | |
| D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0 }; | |
| DXGI_SWAP_CHAIN_DESC swapChainDesc = { {0,0, {}, DXGI_FORMAT_B8G8R8A8_UNORM }, {1 },32,2, window,1 }; | |
| IDXGISwapChain* swapChain; | |
| ID3D11Device* device; | |
| ID3D11DeviceContext* deviceContext; | |
| D3D11CreateDeviceAndSwapChain(0, D3D_DRIVER_TYPE_HARDWARE,0, D3D11_CREATE_DEVICE_BGRA_SUPPORT, featureLevels,1,7, &swapChainDesc, &swapChain, &device,0, &deviceContext); | |
| swapChain->GetDesc(&swapChainDesc); | |
| ID3D11Texture2D* framebuffer; | |
| swapChain->GetBuffer(0,__uuidof(ID3D11Texture2D), (void**)&framebuffer); | |
| ID3D11RenderTargetView* framebufferRTV; | |
| device->CreateRenderTargetView(framebuffer,0, &framebufferRTV); | |
| D3D11_TEXTURE2D_DESC textureDesc = {17,25,1,1, DXGI_FORMAT_R8_UNORM, {1 }, D3D11_USAGE_DYNAMIC,8,65536 }; | |
| ID3D11Texture2D* texture; | |
| device->CreateTexture2D(&textureDesc,0, &texture); | |
| ID3D11ShaderResourceView* textureSRV; | |
| device->CreateShaderResourceView(texture,0, &textureSRV); | |
| ID3DBlob* cso; | |
| D3DCompileFromFile(L"shaders.hlsl",0,0,"mainvs","vs_5_0",0,0, &cso,0); | |
| ID3D11VertexShader* mainVS; | |
| device->CreateVertexShader(cso->GetBufferPointer(), cso->GetBufferSize(),0, &mainVS); | |
| D3DCompileFromFile(L"shaders.hlsl",0,0,"mainps","ps_5_0",0,0, &cso,0); | |
| ID3D11PixelShader* mainPS; | |
| device->CreatePixelShader(cso->GetBufferPointer(), cso->GetBufferSize(),0, &mainPS); | |
| D3D11_VIEWPORT framebufferVP = {0,0, (float)swapChainDesc.BufferDesc.Width, (float)swapChainDesc.BufferDesc.Height,0,1 }; | |
| ((byte*)skullTexture)[0xe5] = (byte)((framebufferVP.Height / framebufferVP.Width) *0xff); | |
| deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); | |
| deviceContext->VSSetShader(mainVS,0,0); | |
| deviceContext->VSSetShaderResources(0,1, &textureSRV); | |
| deviceContext->RSSetViewports(1, &framebufferVP); | |
| deviceContext->PSSetShader(mainPS,0,0); | |
| deviceContext->PSSetShaderResources(0,1, &textureSRV); | |
| deviceContext->OMSetRenderTargets(1, &framebufferRTV,0); | |
| while (true) | |
| { | |
| MSG msg; | |
| while (PeekMessageA(&msg,0,0,0, PM_REMOVE)) {if (msg.message == WM_KEYDOWN)return0;DispatchMessageA(&msg); } | |
| D3D11_MAPPED_SUBRESOURCE msr; | |
| deviceContext->Map(texture,0, D3D11_MAP_WRITE_DISCARD,0, &msr); | |
| for (int i =0; i <25; i++)memcpy(((byte*)msr.pData) + i * msr.RowPitch, ((byte*)skullTexture) + i *17,17); | |
| deviceContext->Unmap(texture,0); | |
| ((byte*)skullTexture)[0xd7]++; | |
| deviceContext->Draw(4,0); | |
| swapChain->Present(1,0); | |
| } | |
| } |
| struct pixel {float4 pos :SV_POSITION;float2 tex : TEX; };// tex = uv * texturesize | |
| /////////////////////////////////////////////////////////////////////////////////////////////////// | |
| Texture2D<float> skulltexture :register(t0); | |
| SamplerState nullsampler :register(s0); | |
| /////////////////////////////////////////////////////////////////////////////////////////////////// | |
| pixelmainvs(uint vertexid :SV_VERTEXID) | |
| { | |
| uint2 index = { vertexid &2, (vertexid <<1 &2) ^3 }; | |
| float2 ratio =float2(17.0f /25.0f,1.0f / skulltexture[uint2(8,13)]); | |
| float4 coord = ratio.xyxy * (smoothstep(-1,1,cos(skulltexture[uint2(11,12)] * -6.2585f)) +1) *float2(-1,1).xyyx /16; | |
| pixel p = {float4(float2(coord[index.x], coord[index.y]),0,1),float2(17,25) * (index >>1) }; | |
| return p; | |
| } | |
| float4mainps(pixel p) :SV_TARGET | |
| { | |
| float2 pix =floor(p.tex) +min(frac(p.tex) /fwidth(p.tex),1) -0.5;// aa point sampling. THIS IS THE MAIN EVENT | |
| return skulltexture.Sample(nullsampler, pix /float2(17,25)); | |
| } |
| /* | |
| float4 mainps(pixel p) : SV_TARGET // regular point sampling vs aa point sampling | |
| { | |
| float2 pix; | |
| if (p.tex.x < 8.5f - fwidth(p.tex.x)) pix = floor(p.tex) + 0.5; // regular point sampling | |
| else if (p.tex.x > 8.5f) pix = floor(p.tex) + min(frac(p.tex) / fwidth(p.tex), 1) - 0.5; // aa point sampling | |
| else return 0; | |
| return skulltexture.Sample(nullsampler, pix / float2(17, 25)); | |
| } | |
| */ |
marpe commentedDec 27, 2022
Thanks, this was helpful 👍 Any tips on how to prevent adjacent pixels "bleeding" into others when using this technique to render a tile map?
d7samurai commentedDec 28, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
an alternative to rendering each tile as a separate antialiased sprite would be to first render the tiles normally, pixel-aligned, to a separate texture, then transform and render that to the screen with aa applied. in general, remember to use premultiplied alpha textures and keep a single-pixel transparent border around them or (for single-sprite textures) set the texture address mode to BORDER and provide a 0-value there
marpe commentedDec 29, 2022
ah, rendering normally and then with aa applied sounds reasonable. I experimented with adding a border, but that will instead produce gaps for tiles that are meant to be rendered right next to each other (better explained by the screenshot). extending the tiles into the border to prevent the gaps just feels a bit tedious, so I'll try out your suggestion.
d7samurai commentedDec 29, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
sure, rendering to an intermediate texture is perhaps the most straightforward in your case. and yes, if you add borders, you would have to render the sprites with a corresponding overlap (which, to be fair, shouldn't be more complicated than subtracting (1, 1) from their regular pixel coordinates.
mmozeiko commentedDec 29, 2022
For rendering texture from atlas simply clamp coordinates to +0.5/-0.5 texel from borders of tile. Exact same sampling code will work, just clamp on uv needed. For example - if you have 4x4 texture where top right corner from (2,0)-(4,2) is one tile, clamp uv to (2.5,0.5)-(3.5,1.5) interval. No padding will be needed. Tiles can touch other tiles on exact pixel borders.
d7samurai commentedDec 29, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
while the above is true for traditional atlas rendering it won't work for this technique, which relies upon sampling into neighboring pixels for aa interpolation. however, if you choose to first render your tiles into an intermediate texture using point sampling, it'll let you skip those borders.

