One of the most common applications of Perlin Noise is in blending textures to achieve more natural appearances. For instance, combining textures like grass and dirt can create a seamless transition between the two, enhancing the realism of a scene. Below, we explore how to implement this using Perlin Noise.

Refer to this well-regarded article, "How to Use Perlin Noise in Your Games", which showcases various techniques including texture blending. However, the article's examples, particularly the code for blending using an interpolation function, lack clarity and practical implementation details. Often, the interpolation function details are omitted, making it hard to replicate the results. Moreover, a more suitable method for such blending would be alpha blending to achieve smoother transitions.
In this blog, I will demonstrate how to recreate the texture blending technique in C++.
Setting Up
Handling images in C++, unlike in higher-level languages, can be somewhat complex. For simplicity, I will use the PPM (Portable Pix Map) P3 file format, which is straightforward as it consists of pixel size and RGB values, making it easier to manipulate.
For importing images, many online services convert any image file to a binary-encoded PPM P6 file format. You can then convert P6 to P3 using this online PPM Converter.
The code goes through a serial operations to convert PPM P3 file to a RGB data type (tagRGB). There is also a function to find a size of the image.
typedef struct tagRBG
{
int ir;
int ig;
int ib;
}RGB;
RGB** PPM_to_RGB(string _sFilePath)
{
FILE* pLoadFile = NULL;
errno_t errLoad = fopen_s(&pLoadFile, _sFilePath.c_str(), "rb");
if (0 == errLoad)
{
fseek(pLoadFile, 0, SEEK_END);
const int iSize = ftell(pLoadFile);
char* cArray = new char[iSize];
rewind(pLoadFile);
fread(cArray, sizeof(char), iSize, pLoadFile);
fclose(pLoadFile);
string s = cArray;
s.resize(iSize);
int iFirstPoint = s.find('\\n');
int iSecondPoint = s.find(' ');
int iPixel_x_size = stoi(s.substr(iFirstPoint + 1, iSecondPoint - iFirstPoint - 1));
iFirstPoint = iSecondPoint;
iSecondPoint = s.find('\\n', iFirstPoint + 1);
int iPixel_y_size = stoi(s.substr(iFirstPoint + 1, iSecondPoint - iFirstPoint - 1));
RGB** tempRBG = new RGB* [iPixel_x_size];
for (int i = 0; i < iPixel_x_size; i++)
{
tempRBG[i] = new RGB[iPixel_y_size];
}
int iXcount(0), iYcount(0);
s.erase(0, 15);
int j(0), k(0), iRGBCount(0);
string sTemp = "";
for (int i = 0; i < s.length() - 1; ++i)
{
if (sTemp.empty() == true)
if (s[i] == ' ' || s[i] == '\\n')
continue;
if (s[i] >= 48 && s[i] <= 57)
sTemp += s[i];
else
{
if (iRGBCount == 0)
{
tempRBG[k][j].ir = stoi(sTemp);
++iRGBCount;
}
else if (iRGBCount == 1)
{
tempRBG[k][j].ig = stoi(sTemp);
++iRGBCount;
}
else
{
tempRBG[k][j].ib= stoi(sTemp);
iRGBCount = 0;
}
sTemp = "";
if (iRGBCount == 0)
++j;
}
if (j == iPixel_y_size)
{
if (iRGBCount == 0)
{
++k;
j = 0;
}
}
}
delete[] cArray;
cArray = nullptr;
return tempRBG;
}
return nullptr;
}
int* Get_Pixel_Size(string _sFilePath)
{
int* iArray = new int[2] {0};
FILE* pLoadFile = NULL;
errno_t errLoad = fopen_s(&pLoadFile, _sFilePath.c_str(), "rb");
if (0 == errLoad)
{
fseek(pLoadFile, 0, SEEK_END);
const int iSize = ftell(pLoadFile);
char* cArray = new char[iSize];
rewind(pLoadFile);
fread(cArray, sizeof(char), iSize, pLoadFile);
fclose(pLoadFile);
string s = cArray;
s.resize(iSize);
//Find Pixel Size
int iFirstPoint = s.find('\\n');
int iSecondPoint = s.find(' ');
iArray[0] = stoi(s.substr(iFirstPoint + 1, iSecondPoint - iFirstPoint - 1));
iFirstPoint = iSecondPoint;
iSecondPoint = s.find('\\n', iFirstPoint + 1);
iArray[1] = stoi(s.substr(iFirstPoint + 1, iSecondPoint - iFirstPoint - 1));
return iArray;
}
return nullptr;
}
Implementing the Blending
Following the approach in the DevMag article, I used similar image textures. To achieve more natural results, I applied Perlin Noise with octaves to create rougher outlines and clipped the output values for more dramatic contrasts.


Below is the function to map and clip output values, and the code for alpha blending:
float Map_n_Clip(float _fInput, float _fStart, float _fEnd, float _fNewStart, float _fNewEnd)
{
float iMapped = _fNewStart + (_fInput - _fStart) * (_fNewEnd - _fNewStart) / (_fEnd - _fStart);
return std::max(_fNewStart, std::min(iMapped, _fNewEnd));
}
int Map_n_Clip(float _fInput, float _fStart, float _fEnd, int _iNewStart, int _iNewEnd)
{
int iMapped = _iNewStart + (_fInput - _fStart) * (_iNewEnd - _iNewStart) / (_fEnd - _fStart);
return std::max(_iNewStart, std::min(iMapped, _iNewEnd));
}
Alpha blending techique is quite simple. The math equation and implemented code are shown below.
Alpha Blend = Alpha × MainValue + (1-Alpha) × SubValue
int Alpha_Blend(int _iMain, int _iSub, float _fPerlinValue)
{
return _iMain * _fPerlinValue + _iSub * (1 - _fPerlinValue);
}
Result and Conclusion
Adjusting the Perlin Noise parameters such as increment (scale), the number of octaves, and frequency is crucial for obtaining the desired effect. Clipping the output values is equally important to ensure clear differentiation between textures like grass and dirt.
iH_PN_Map = Map_n_Clip(Octave_Perlin(fXoff, fYoff, 4, 0.5), -.4f, .4f, 0, 255);
To effectively apply the clipping method, it is crucial to understand the range and distribution of Perlin Noise values. For a deeper dive into this topic, keep an eye out for an upcoming post titled "Understanding the Perlin Noise Value Range and Distribution."
Anyhow, the final outcomes are shown below. The full code also shown.
Final Code
int main()
{
string sDirtPath = "Texture/Dirt.ppm";
string sGrassPath = "Texture/Grass.ppm";
int* iDirtSize = Get_Pixel_Size(sDirtPath);
RGB** ppDirth = PPM_to_RGB(sDirtPath);
int* iGrassSize = Get_Pixel_Size(sGrassPath);
RGB** ppGrass = PPM_to_RGB(sGrassPath);
if (iDirtSize[0] != iGrassSize[0] || iDirtSize[1] != iGrassSize[1])
{
cout << " Images are not competible " << endl;
return 0;
}
const int iImageWidth = iDirtSize[0];
const int iImageHeight = iDirtSize[1];
float fXoff(0.f), fYoff(0.f);
float fIncrement(0.007f);
RGB rbgCombined = {};
RGB rbgPerlin = {};
std::ofstream Perlin_Noise_Map;
std::ofstream Combined_Image;
Perlin_Noise_Map.open("Perlin_Noise_Map.ppm");
Combined_Image.open("Combined_Image.ppm");
if (Perlin_Noise_Map.is_open()) {
Perlin_Noise_Map << "P3\\n" << iImageWidth << ' ' << iImageHeight << "\\n255\\n";
Combined_Image << "P3\\n" << iImageWidth << ' ' << iImageHeight << "\\n255\\n";
int ir(0), ig(0), ib(0);
float fH_CI_Map(0.f), fOffset(0.f), fHoles(0.f);
int iH_PN_Map(0);
for (int j = iImageHeight - 1; j >= 0; --j) {
fXoff = 0;
for (int i = 0; i < iImageWidth; ++i)
{
fH_CI_Map = Map_n_Clip(Octave_Perlin(fXoff, fYoff, 4, 0.5), -.4f, .4f, 0.f, 1.f);
iH_PN_Map = Map_n_Clip(Octave_Perlin(fXoff, fYoff, 4, 0.5), -.4f, .4f, 0, 255);
rbgPerlin.ir = iH_PN_Map;
rbgPerlin.ig = iH_PN_Map;
rbgPerlin.ib = iH_PN_Map;
rbgCombined.ir = Alpha_Blend(ppDirth[i][j].ir, ppGrass[i][j].ir, fH_CI_Map);
rbgCombined.ig = Alpha_Blend(ppDirth[i][j].ig, ppGrass[i][j].ig, fH_CI_Map);
rbgCombined.ib = Alpha_Blend(ppDirth[i][j].ib, ppGrass[i][j].ib, fH_CI_Map);
Combined_Image << rbgCombined.ir << ' '
<< rbgCombined.ig << ' '
<< rbgCombined.ib << '\\n';
Perlin_Noise_Map << rbgPerlin.ir << ' '
<< rbgPerlin.ig << ' '
<< rbgPerlin.ib << '\\n';
fXoff += fIncrement;
}
fYoff += fIncrement;
}
}
Perlin_Noise_Map.close();
Combined_Image.close();
Safe_Delete(ppDirth, iDirtSize[0]);
Safe_Delete(ppGrass, iGrassSize[0]);
delete[] iDirtSize;
iDirtSize = nullptr;
delete[] iGrassSize;
iGrassSize = nullptr;
return 0;
}
Comments