.NET环境下使用System.Drawing实现带3D效果的三角形纹理映射
Alright, let's break this down step by step—since you're building a 3D editor and want to map BMP textures to triangles using only System.Drawing, the core technique here is barycentric coordinate-based texture mapping. This method lets you tie each pixel in the triangle to its corresponding point on the texture. Here's exactly how to implement it:
1. Define Your Data Structures
First, create a simple struct to hold both the 2D-projected vertex coordinates (after you've converted your 3D points to screen space) and their matching texture UV coordinates:
public struct TexturedVertex { // 2D screen position (after 3D → 2D projection) public float ScreenX; public float ScreenY; // Texture UV coordinates (0-1 range; (0,0) = top-left of the BMP) public float U; public float V; // Optional: Add a Z value here for depth testing (z-buffering) public float Z; }
2. Load Your BMP Texture
Use System.Drawing.Bitmap to load your texture. For better performance, lock the bitmap's bits directly (avoid slow GetPixel/SetPixel calls):
// Load the BMP texture Bitmap texture = new Bitmap("your-texture-file.bmp"); // Lock texture bits for fast pixel access BitmapData textureData = texture.LockBits( new Rectangle(0, 0, texture.Width, texture.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
3. Implement Barycentric Texture Mapping
The key part is calculating barycentric coordinates for each pixel in the triangle's bounding box. These coordinates let you interpolate the UV values across the triangle, then sample the corresponding texture pixel.
Here's a complete implementation for drawing a textured triangle to a target canvas (your editor's viewport):
// Assume this is your editor's canvas bitmap Bitmap viewport = new Bitmap(1280, 720); BitmapData viewportData = viewport.LockBits( new Rectangle(0, 0, viewport.Width, viewport.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); // Define your triangle's textured vertices (replace with your projected 3D points) TexturedVertex v1 = new TexturedVertex { ScreenX = 200, ScreenY = 150, U = 0, V = 0 }; TexturedVertex v2 = new TexturedVertex { ScreenX = 600, ScreenY = 600, U = 1, V = 1 }; TexturedVertex v3 = new TexturedVertex { ScreenX = 100, ScreenY = 550, U = 0, V = 1 }; // Calculate the triangle's bounding box to limit pixel checks int minX = (int)Math.Min(Math.Min(v1.ScreenX, v2.ScreenX), v3.ScreenX); int maxX = (int)Math.Max(Math.Max(v1.ScreenX, v2.ScreenX), v3.ScreenX); int minY = (int)Math.Min(Math.Min(v1.ScreenY, v2.ScreenY), v3.ScreenY); int maxY = (int)Math.Max(Math.Max(v1.ScreenY, v2.ScreenY), v3.ScreenY); // Clamp bounds to avoid drawing outside the viewport minX = Math.Max(minX, 0); maxX = Math.Min(maxX, viewport.Width - 1); minY = Math.Max(minY, 0); maxY = Math.Min(maxY, viewport.Height - 1); // Precompute denominator for barycentric calculations (avoids repeated division) float denom = ((v2.ScreenY - v3.ScreenY) * (v1.ScreenX - v3.ScreenX)) + ((v3.ScreenX - v2.ScreenX) * (v1.ScreenY - v3.ScreenY)); // Skip degenerate triangles (zero area) if (Math.Abs(denom) < float.Epsilon) goto Cleanup; // Iterate over every pixel in the bounding box for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { // Calculate barycentric weights (alpha, beta, gamma) float alpha = ((v2.ScreenY - v3.ScreenY) * (x - v3.ScreenX) + (v3.ScreenX - v2.ScreenX) * (y - v3.ScreenY)) / denom; float beta = ((v3.ScreenY - v1.ScreenY) * (x - v3.ScreenX) + (v1.ScreenX - v3.ScreenX) * (y - v3.ScreenY)) / denom; float gamma = 1 - alpha - beta; // Check if the pixel is inside the triangle (all weights ≥ 0) if (alpha >= 0 && beta >= 0 && gamma >= 0) { // Interpolate UV coordinates using barycentric weights float u = (alpha * v1.U) + (beta * v2.U) + (gamma * v3.U); float v = (alpha * v1.V) + (beta * v2.V) + (gamma * v3.V); // Convert UV to texture pixel coordinates (clamp to avoid out-of-bounds) int texX = (int)Math.Clamp(u * texture.Width, 0, texture.Width - 1); int texY = (int)Math.Clamp(v * texture.Height, 0, texture.Height - 1); // Get the texture pixel color from locked bits int texPixelOffset = texY * textureData.Stride + texX * 4; Color texColor = Color.FromArgb( Marshal.ReadByte(textureData.Scan0, texPixelOffset + 3), // Alpha Marshal.ReadByte(textureData.Scan0, texPixelOffset + 2), // Red Marshal.ReadByte(textureData.Scan0, texPixelOffset + 1), // Green Marshal.ReadByte(textureData.Scan0, texPixelOffset)); // Blue // Write the color to the viewport bitmap int viewportPixelOffset = y * viewportData.Stride + x * 4; Marshal.WriteByte(viewportData.Scan0, viewportPixelOffset + 3, texColor.A); Marshal.WriteByte(viewportData.Scan0, viewportPixelOffset + 2, texColor.R); Marshal.WriteByte(viewportData.Scan0, viewportPixelOffset + 1, texColor.G); Marshal.WriteByte(viewportData.Scan0, viewportPixelOffset, texColor.B); } } } Cleanup: // Unlock bitmaps to free resources texture.UnlockBits(textureData); viewport.UnlockBits(viewportData); // To display the result, draw the viewport to your editor's Graphics context using (Graphics g = yourEditorPanel.CreateGraphics()) { g.DrawImage(viewport, 0, 0); }
4. Critical Enhancements for 3D Quality
To make this feel truly 3D, add these touches:
- Z-Buffering: Track the depth (Z value) of each pixel. Only draw a pixel if its Z is closer to the camera than the existing pixel in the z-buffer. This fixes occlusion issues (objects behind other objects won't draw over them).
- Perspective-Correct Interpolation: The basic barycentric interpolation above is linear, which can cause texture distortion in perspective. For better results, interpolate
1/Z,U/Z, andV/Z, then computeU = (U/Z)/(1/Z)andV = (V/Z)/(1/Z)for perspective-correct UVs. - Texture Filtering: Add bilinear filtering by sampling the four nearest texture pixels and blending their colors, instead of just picking the closest one. This smooths out jagged texture edges.
Key Notes
- Projection First: Remember, you need to project your 3D vertices to 2D screen coordinates before using this code. Implement a perspective or orthogonal projection matrix to convert 3D world positions to 2D screen positions.
- Performance: Locking bitmap bits is non-negotiable for speed—
GetPixel/SetPixelare way too slow for real-time 3D editing. - UV Range: Ensure your UV coordinates are in the 0-1 range. For texture tiling, replace the clamp with
u % 1andv % 1.
内容的提问来源于stack exchange,提问作者MrOlegus

