本文主要讲述Games101中的着色部分。
文中将使用UE的UTexture2D接口,若不了解可以看这篇:
https://blog.csdn.net/grayrail/article/details/142165442
1.面积比计算三角形坐标
通过三角形面积比可以得到三角形的坐标alpha、beta、gamma从而进行插值,或是进行图像纹理绘制,基于上一篇学习文章的三角形绘制脚本:
https://blog.csdn.net/grayrail/article/details/142211284
增加面积比插值后脚本如下:
#include "MyBlueprintFunctionLibrary.h"
float TriangleArea(const FVector2D& A, const FVector2D& B, const FVector2D& C)
{
// 计算向量AB和AC的叉乘
float CrossProduct = (B.X - A.X) * (C.Y - A.Y) - (B.Y - A.Y) * (C.X - A.X);
// 返回三角形面积(取叉乘结果的绝对值的一半)
return FMath::Abs(CrossProduct) * 0.5f;
}
// 计算P点的重心坐标
FVector GetBarycentricCoordinates(const FVector2D& P, const FVector2D& A, const FVector2D& B, const FVector2D& C)
{
// 计算ABC的总面积
float AreaABC = TriangleArea(A, B, C);
// 计算PBC, PCA, PAB的面积
float AreaPBC = TriangleArea(P, B, C);
float AreaPCA = TriangleArea(P, C, A);
float AreaPAB = TriangleArea(P, A, B);
// 重心坐标
float alpha = AreaPBC / AreaABC;
float beta = AreaPCA / AreaABC;
float gamma = AreaPAB / AreaABC;
return FVector(alpha, beta, gamma); // 返回重心坐标(alpha, beta, gamma)
}
UTexture2D* UMyBlueprintFunctionLibrary::GenTexture(int32 Width, int32 Height)
{
// 创建临时纹理
UTexture2D* NewTexture = UTexture2D::CreateTransient(Width, Height);
// 配置纹理
NewTexture->MipGenSettings = TMGS_NoMipmaps;
NewTexture->CompressionSettings = TC_VectorDisplacementmap;
NewTexture->SRGB = false;
// 锁定纹理数据进行写入
FTexture2DMipMap& Mip = NewTexture->PlatformData->Mips[0];
void* TextureData = Mip.BulkData.Lock(LOCK_READ_WRITE);
// 设置默认颜色为黑色
FColor* FormattedImageData = static_cast<FColor*>(TextureData);
for (int32 y = 0; y < Height; ++y)
{
for (int32 x = 0; x < Width; ++x)
{
FormattedImageData[y * Width + x] = FColor::Black; // 背景颜色设置为黑色
}
}
// 定义三角形顶点(A, B, C)
FVector2D A(Width / 2, Height / 4); // 三角形顶点A
FVector2D B(Width / 4, 3 * Height / 4); // 三角形顶点B
FVector2D C(3 * Width / 4, 3 * Height / 4); // 三角形顶点C
// 深红色
FColor TriangleColor = FColor(139, 0, 0, 255);
// 叉乘判断点P是否在三角形ABC内
auto IsPointInTriangle = [](const FVector2D& P, const FVector2D& A, const FVector2D& B, const FVector2D& C) -> bool
{
FVector2D AP = P - A;
FVector2D BP = P - B;
FVector2D CP = P - C;
FVector2D AB = B - A;
FVector2D BC = C - B;
FVector2D CA = A - C;
// 叉乘结果
float Cross1 = AB.X * AP.Y - AB.Y * AP.X; // AB 和 AP 的叉乘
float Cross2 = BC.X * BP.Y - BC.Y * BP.X; // BC 和 BP 的叉乘
float Cross3 = CA.X * CP.Y - CA.Y * CP.X; // CA 和 CP 的叉乘
// 如果三个叉乘结果符号相同,则点在三角形内
return (Cross1 >= 0 && Cross2 >= 0 && Cross3 >= 0) || (Cross1 <= 0 && Cross2 <= 0 && Cross3 <= 0);
};
int SubPixelCount = 8;
// 超采样抗锯齿:子像素划分
float SubPixelStep = 1.0f / SubPixelCount; // 子像素的步长
int32 TotalSubPixels = SubPixelCount * SubPixelCount; // 子像素的总数
// 遍历每个像素并应用抗锯齿逻辑
for (int32 y = 0; y < Height; ++y)
{
for (int32 x = 0; x < Width; ++x)
{
int32 CoveredSubPixels = 0;
// 遍历 SubPixelCount x SubPixelCount 子像素
for (int32 subY = 0; subY < SubPixelCount; ++subY)
{
for (int32 subX = 0; subX < SubPixelCount; ++subX)
{
FVector2D SubPixelPos = FVector2D(x + (subX + 0.5f) * SubPixelStep, y + (subY + 0.5f) * SubPixelStep); // 子像素位置
if (IsPointInTriangle(SubPixelPos, A, B, C))
{
CoveredSubPixels++;
}
}
}
// 计算覆盖率并设置像素颜色
float Coverage = static_cast<float>(CoveredSubPixels) / TotalSubPixels; // 覆盖率(0 到 1)
if (Coverage > 0)
{
FVector2D P(x, y);
FVector BaryCoords = GetBarycentricCoordinates(P, A, B, C);
BaryCoords.X = FMath::RoundToInt(BaryCoords.X * 255 * Coverage);
BaryCoords.Y = FMath::RoundToInt(BaryCoords.Y * 255 * Coverage);
BaryCoords.Z = FMath::RoundToInt(BaryCoords.Z * 255 * Coverage);
FColor FinalColor = FColor(BaryCoords.X, BaryCoords.Y, BaryCoords.Z);
FormattedImageData[y * Width + x] = FinalColor;
}
}
}
// 解锁纹理数据
Mip.BulkData.Unlock();
NewTexture->UpdateResource();
return NewTexture;
}
绘制结果如下:
此外,gamma插值信息的获取,也可以这样修改:
// 重心坐标
float alpha = AreaPBC / AreaABC;
float beta = AreaPCA / AreaABC;
float gamma = 1 - alpha - beta;
2.双线性插值 Bilinear
因为实际屏幕采样像素时需要考虑到材质大小、屏幕占比等因素,采样图片并不会像CPU采样那样都是整数,而这种0-1浮点数的坐标采样会带来走样问题,因此通过Bilinear双线性采样的方式采样周围4个像素并进行插值,从而得到更好的采样结果:
代码如下:
UTexture2D* UMyBlueprintFunctionLibrary::GenTexture(int32 Width, int32 Height, UTexture2D* SourceTexture)
{
if (!SourceTexture)
{
UE_LOG(LogTemp, Error, TEXT("SourceTexture is null"));
return nullptr;
}
// 创建一个新的UTexture2D
UTexture2D* NewTexture = UTexture2D::CreateTransient(Width, Height, SourceTexture->GetPixelFormat());
if (!NewTexture)
{
UE_LOG(LogTemp, Error, TEXT("Failed to create new texture"));
return nullptr;
}
// 锁定源纹理和新纹理的内存
FTexture2DMipMap& SourceMip = SourceTexture->PlatformData->Mips[0];
FTexture2DMipMap& DestMip = NewTexture->PlatformData->Mips[0];
// 获取源纹理的像素数据
uint8* SourcePixels = static_cast<uint8*>(SourceMip.BulkData.Lock(LOCK_READ_ONLY));
uint8* DestPixels = static_cast<uint8*>(DestMip.BulkData.Lock(LOCK_READ_WRITE));
int32 SourceWidth = SourceMip.SizeX;
int32 SourceHeight = SourceMip.SizeY;
int32 PixelSize = 4; // 假设使用的是标准的8位RGBA纹理
// 进行采样并将数据写入到新纹理中
for (int32 y = 0; y < Height; ++y)
{
for (int32 x = 0; x < Width; ++x)
{
// 计算源纹理中的浮动坐标
float U = static_cast<float>(x) / Width * (SourceWidth - 1);
float V = static_cast<float>(y) / Height * (SourceHeight - 1);
// 获取四个邻近像素的坐标
int32 X0 = static_cast<int32>(FMath::FloorToInt(U));
int32 X1 = FMath::Clamp(X0 + 1, 0, SourceWidth - 1);
int32 Y0 = static_cast<int32>(FMath::FloorToInt(V));
int32 Y1 = FMath::Clamp(Y0 + 1, 0, SourceHeight - 1);
// 获取插值因子
float FracX = U - X0;
float FracY = V - Y0;
// 计算四个顶点的索引
int32 Index00 = (Y0 * SourceWidth + X0) * PixelSize;
int32 Index01 = (Y1 * SourceWidth + X0) * PixelSize;
int32 Index10 = (Y0 * SourceWidth + X1) * PixelSize;
int32 Index11 = (Y1 * SourceWidth + X1) * PixelSize;
// 线性插值四个像素点
auto Lerp = [](uint8 A, uint8 B, float T) -> uint8 {
return FMath::Clamp(static_cast<int32>(FMath::Lerp(static_cast<float>(A), static_cast<float>(B), T)), 0, 255);
};
// 对R、G、B、A分别进行插值
uint8 R = Lerp(Lerp(SourcePixels[Index00], SourcePixels[Index10], FracX),
Lerp(SourcePixels[Index01], SourcePixels[Index11], FracX),
FracY);
uint8 G = Lerp(Lerp(SourcePixels[Index00 + 1], SourcePixels[Index10 + 1], FracX),
Lerp(SourcePixels[Index01 + 1], SourcePixels[Index11 + 1], FracX),
FracY);
uint8 B = Lerp(Lerp(SourcePixels[Index00 + 2], SourcePixels[Index10 + 2], FracX),
Lerp(SourcePixels[Index01 + 2], SourcePixels[Index11 + 2], FracX),
FracY);
uint8 A = Lerp(Lerp(SourcePixels[Index00 + 3], SourcePixels[Index10 + 3], FracX),
Lerp(SourcePixels[Index01 + 3], SourcePixels[Index11 + 3], FracX),
FracY);
// 写入目标纹理
int32 DestIndex = (y * Width + x) * PixelSize;
DestPixels[DestIndex] = R;
DestPixels[DestIndex + 1] = G;
DestPixels[DestIndex + 2] = B;
DestPixels[DestIndex + 3] = A;
}
}
// 解锁像素数据
SourceMip.BulkData.Unlock();
DestMip.BulkData.Unlock();
// 更新新纹理
NewTexture->UpdateResource();
return NewTexture;
}
注意需要更新蓝图节点: