在DirectX 11中使用FreeType绘制文本的转换难题
解决FreeType位图转DirectX11纹理并实现
my_draw_bitmap 我来帮你搞定这个FT_Bitmap转ID3D11Texture2D的问题,还有my_draw_bitmap函数的实现——这确实是FreeType+DirectX11文本渲染里最关键的一步。下面是一步步的落地方案:
1. 先搞懂格式匹配逻辑
FreeType输出的FT_Bitmap默认是8位Alpha通道数据(pixel_mode为FT_PIXEL_MODE_GRAY),0代表完全透明,255代表完全不透明。我们可以用DirectX的DXGI_FORMAT_A8_UNORM格式完美匹配这个数据。
2. 核心转换与渲染函数实现
下面是my_draw_bitmap的完整逻辑,包含FT_Bitmap到D3D11纹理的转换,以及单个字形的渲染:
#include <DirectXMath.h> #include <d3d11.h> #include <vector> // 辅助函数:把FT_Bitmap转成D3D11纹理和对应的着色器资源视图 HRESULT ConvertFTBitmapToD3DTexture(ID3D11Device* pDevice, ID3D11DeviceContext* pContext, const FT_Bitmap& bitmap, ID3D11Texture2D** ppTexture, ID3D11ShaderResourceView** ppSRV) { // 1. 填充纹理描述 D3D11_TEXTURE2D_DESC texDesc = {}; texDesc.Width = bitmap.width; texDesc.Height = bitmap.rows; texDesc.MipLevels = 1; texDesc.ArraySize = 1; texDesc.Format = DXGI_FORMAT_A8_UNORM; // 对齐FreeType的Alpha通道格式 texDesc.SampleDesc.Count = 1; texDesc.Usage = D3D11_USAGE_DYNAMIC; // 动态更新,适配不同字形 texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // 2. 处理像素对齐:D3D要求纹理行宽是4的倍数,FreeType的pitch可能不满足 UINT rowPitch = (bitmap.width * sizeof(unsigned char) + 3) & ~3; std::vector<unsigned char> alignedPixels(rowPitch * bitmap.rows, 0); // 把FreeType的原始数据拷贝到对齐后的缓冲区 for (UINT y = 0; y < bitmap.rows; ++y) { memcpy(&alignedPixels[y * rowPitch], bitmap.buffer + y * bitmap.pitch, bitmap.width); } // 3. 准备初始数据 D3D11_SUBRESOURCE_DATA initData = {}; initData.pSysMem = alignedPixels.data(); initData.SysMemPitch = rowPitch; // 4. 创建纹理 HRESULT hr = pDevice->CreateTexture2D(&texDesc, &initData, ppTexture); if (FAILED(hr)) return hr; // 5. 创建着色器资源视图,让着色器能采样这个纹理 D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Format = texDesc.Format; srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MipLevels = 1; return pDevice->CreateShaderResourceView(*ppTexture, &srvDesc, ppSRV); } // 你的my_draw_bitmap函数:渲染单个字形位图 void my_draw_bitmap(ID3D11Device* pDevice, ID3D11DeviceContext* pContext, const FT_Bitmap& bitmap, FT_Vector penPosition, ID3D11Buffer* pVertexBuffer, ID3D11VertexShader* pVS, ID3D11PixelShader* pPS, ID3D11InputLayout* pInputLayout) { ID3D11Texture2D* pGlyphTex = nullptr; ID3D11ShaderResourceView* pGlyphSRV = nullptr; // 转换位图为D3D纹理 if (FAILED(ConvertFTBitmapToD3DTexture(pDevice, pContext, bitmap, &pGlyphTex, &pGlyphSRV))) { // 这里可以加错误日志 return; } // 把FreeType的1/64像素单位转成实际屏幕像素 float x = static_cast<float>(penPosition.x) / 64.0f; float y = static_cast<float>(penPosition.y) / 64.0f; // 定义带纹理坐标的顶点结构 struct Vertex { DirectX::XMFLOAT2 pos; DirectX::XMFLOAT2 uv; }; // 构建字形的四边形顶点(左上、右上、左下、右下,用三角带渲染) Vertex vertices[] = { { {x, y}, {0.0f, 0.0f} }, // 左上 { {x + bitmap.width, y}, {1.0f, 0.0f} }, // 右上 { {x, y - bitmap.rows}, {0.0f, 1.0f} }, // 左下 { {x + bitmap.width, y - bitmap.rows}, {1.0f, 1.0f} } // 右下 }; // 更新顶点缓冲区 D3D11_MAPPED_SUBRESOURCE mappedResource; if (SUCCEEDED(pContext->Map(pVertexBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource))) { memcpy(mappedResource.pData, vertices, sizeof(vertices)); pContext->Unmap(pVertexBuffer, 0); } // 设置渲染管线状态 pContext->IASetInputLayout(pInputLayout); pContext->IASetVertexBuffers(0, 1, &pVertexBuffer, nullptr, nullptr); pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); pContext->VSSetShader(pVS, nullptr, 0); pContext->PSSetShader(pPS, nullptr, 0); pContext->PSSetShaderResources(0, 1, &pGlyphSRV); // 渲染这个字形的四边形 pContext->Draw(4, 0); // 释放临时资源(实际项目建议缓存字形纹理,避免频繁创建销毁) pGlyphSRV->Release(); pGlyphTex->Release(); }
3. 在你的RenderText函数中整合调用
在FontLoader::RenderText里,遍历每个字符,加载字形并调用my_draw_bitmap,同时处理字符间距:
void FontLoader::RenderText(ID3D11Device* p_device, ID3D11DeviceContext* p_context, Text* p_text, Math::Vec2 p_position) { const char* text = p_text->GetText(); // 假设你的Text类提供获取文本的方法 FT_Face face = /* 你的FreeType字体Face对象,需要提前初始化 */; // 初始化笔位置(转成FreeType的1/64像素单位) FT_Vector pen; pen.x = static_cast<FT_Pos>(p_position.x * 64.0f); pen.y = static_cast<FT_Pos>(p_position.y * 64.0f); for (size_t i = 0; text[i] != '\0'; ++i) { // 加载当前字符的字形并渲染成位图 if (FT_Load_Char(face, text[i], FT_LOAD_RENDER) != 0) { continue; // 跳过加载失败的字符 } FT_GlyphSlot slot = face->glyph; FT_Bitmap& bitmap = slot->bitmap; // 调整笔位置:考虑字形的bearing(字形相对于笔位置的偏移) FT_Vector glyphPen; glyphPen.x = pen.x + slot->bitmap_left * 64; glyphPen.y = pen.y - slot->bitmap_top * 64; // 渲染当前字形 my_draw_bitmap(p_device, p_context, bitmap, glyphPen, /* 你的全局顶点缓冲区 */, /* 你的顶点着色器 */, /* 你的像素着色器 */, /* 你的输入布局 */); // 更新笔位置到下一个字符 pen.x += slot->advance.x; pen.y += slot->advance.y; } }
必加的优化与注意事项
- 资源缓存:上面的示例每次渲染都创建新纹理,性能很差。实际项目要缓存已经渲染过的字形纹理,比如用
std::unordered_map<char, ID3D11ShaderResourceView*>存储。 - Alpha混合:要开启透明混合才能正确显示文本,需要创建并设置混合状态:
D3D11_BLEND_DESC blendDesc = {}; blendDesc.RenderTarget[0].BlendEnable = TRUE; blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; ID3D11BlendState* pBlendState = nullptr; p_device->CreateBlendState(&blendDesc, &pBlendState); p_context->OMSetBlendState(pBlendState, nullptr, 0xFFFFFFFF); - 字体大小适配:如果需要缩放文本,要提前用
FT_Set_Pixel_Sizes调整FreeType的输出位图大小。
内容的提问来源于stack exchange,提问作者aqwerty
相关产品推荐
相关产品推荐

