Movatterモバイル変換


[0]ホーム

URL:


Skip to content
Search Gists
Sign in Sign up

Instantly share code, notes, and snippets.

@d7samurai
Last activeDecember 9, 2025 02:30
    • Star(35)You must be signed in to star a gist
    • Fork(0)You must be signed in to fork a gist

    Select an option

    Save d7samurai/9f17966ba6130a75d1bfb0f1894ed377 to your computer and use it in GitHub Desktop.
    Minimal D3D11 bonus material: pixel art antialiasing

    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.

    skull

    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


    TexPrep

    texprep cmd

    *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
    Copy link

    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
    Copy link
    Author

    d7samurai commentedDec 28, 2022
    edited
    Loading

    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
    Copy link

    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.
    pixel-gap

    @d7samurai
    Copy link
    Author

    d7samurai commentedDec 29, 2022
    edited
    Loading

    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
    Copy link

    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
    Copy link
    Author

    d7samurai commentedDec 29, 2022
    edited
    Loading

    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.

    Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

    [8]ページ先頭

    ©2009-2025 Movatter.jp