Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 71 additions & 167 deletions src/CoreLib/Image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#include "MernelPlatform/ByteOrderStream.hpp"

#include <numeric>
#include <algorithm>
#include <cmath>

namespace D2ModGen::Image {
Expand Down Expand Up @@ -67,158 +67,6 @@ uint8_t s_defaultPaletteGBR[768] = {
// clang-format on
};

class StoneTextureGenerator {
private:
int p[512];

// Standard Fade function for smooth transitions
double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); }

// Linear interpolation
double lerp(double t, double a, double b) { return a + t * (b - a); }

// Gradient calculation
double grad(int hash, double x, double y)
{
int h = hash & 7;
double u = h < 4 ? x : y;
double v = h < 4 ? y : x;
return ((h & 1) ? -u : u) + ((h & 2) ? -2.0 * v : 2.0 * v);
}

public:
StoneTextureGenerator(unsigned int seed = 42)
{
// Initialize permutation table with a standard shuffle sequence
int base_p[256];
std::iota(std::begin(base_p), std::end(base_p), 0);

// Simple LCG deterministic shuffle using the seed
for (int i = 255; i > 0; --i) {
seed = seed * 1103515245 + 12345;
int j = (seed / 65536) % (i + 1);
std::swap(base_p[i], base_p[j]);
}

// Duplicate the array to avoid buffer overflows during interpolation
for (int i = 0; i < 256; i++) {
p[i] = base_p[i];
p[i + 256] = base_p[i];
}
}

// 2D Perlin Noise implementation returning range [-1.0, 1.0]
double noise(double x, double y)
{
int X = (int) std::floor(x) & 255;
int Y = (int) std::floor(y) & 255;

x -= std::floor(x);
y -= std::floor(y);

double u = fade(x);
double v = fade(y);

int A = p[X] + Y;
int B = p[X + 1] + Y;

return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)), lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1)));
}

// Generates a 32-bit RGBA color buffer matching the UI element dimensions
void generateStoneTexture(Bitmap& bitmap, double scale = 0.03)
{
int width = bitmap.size.width;
int height = bitmap.size.height;

for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
double sampleX = x * scale;
double sampleY = y * scale;

double totalHeight = 0.0;
double slopeX = 0.0;
double slopeY = 0.0;

double amplitude = 1.0;
double frequency = 1.0;
double maxValue = 0.0;

// 3 octaves for high speed while preserving micro-cracks
for (int octave = 0; octave < 3; ++octave) {
double nx = sampleX * frequency;
double ny = sampleY * frequency;

// --- FAST ANALYTICAL INLINE DERIVATIVE ---
int X = (int) std::floor(nx) & 255;
int Y = (int) std::floor(ny) & 255;
double fx = nx - std::floor(nx);
double fy = ny - std::floor(ny);

// Inline fade curves and their exact derivatives
double u = fx * fx * fx * (fx * (fx * 6 - 15) + 10);
double v = fy * fy * fy * (fy * (fy * 6 - 15) + 10);
double du = 30.0 * fx * fx * (fx * (fx - 2.0) + 1.0);
double dv = 30.0 * fy * fy * (fy * (fy - 2.0) + 1.0);

int A = p[X] + Y;
int B = p[X + 1] + Y;
double g00 = grad(p[A], fx, fy);
double g10 = grad(p[B], fx - 1, fy);
double g01 = grad(p[A + 1], fx, fy - 1);
double g11 = grad(p[B + 1], fx - 1, fy - 1);

// Calculate smooth height
double r = lerp(v, lerp(u, g00, g10), lerp(u, g01, g11));

// Transform to Ridged/Craggy profile mathematically
double crag = 1.0 - std::abs(r);
double cragSq = crag * crag;

totalHeight += cragSq * amplitude;
maxValue += amplitude;

// Chain rule to extract matching slopes instantly without extra noise calls
double dNdx = lerp(v, g10 - g00, g11 - g01) * du;
double dNdy = (lerp(u, g01, g11) - lerp(u, g00, g10)) * dv;

// Sign correction based on the absolute value flip
double sign = (r < 0.0) ? 1.0 : -1.0;
slopeX += 2.0 * crag * sign * dNdx * amplitude * frequency;
slopeY += 2.0 * crag * sign * dNdy * amplitude * frequency;

amplitude *= 0.45;
frequency *= 2.2;
}

totalHeight /= maxValue;

// --- FAKE LIGHTING CALCULATIONS ---
double lightX = -0.5;
double lightY = -0.5;
double lightZ = 0.8;
double rsq = slopeX * slopeX + slopeY * slopeY + 1.0;
double inv_len = 1.0 / std::sqrt(rsq);

double light = (slopeX * inv_len) * lightX + (slopeY * inv_len) * lightY + inv_len * lightZ;
light = std::max(0.0, light);

double rockPattern = std::pow(totalHeight, 2.5);
double finalShade = rockPattern * (0.3 + 0.7 * light);

// --- COMPATIBILITY WITH YOUR BITMAP API ---
Color c;
c.r = static_cast<int>(12 + finalShade * 40); // Range ~12 to 52 (Dark Charcoal)
c.g = static_cast<int>(14 + finalShade * 42);
c.b = static_cast<int>(16 + finalShade * 45);
c.a = 255;

bitmap.putDot({ x, y }, c);
}
}
}
};

void makeBorderImage(Bitmap& dest, const Bitmap& src, Size cornerSize)
{
const int srcW = src.size.width;
Expand Down Expand Up @@ -301,13 +149,28 @@ Point Point::operator+(const Point& s)
return result;
}

Point Point::operator-(const Point& s)
{
Point result = *this;
result.x -= s.x;
result.y -= s.y;
return result;
}

Point& Point::operator+=(const Point& r)
{
x += r.x;
y += r.y;
return *this;
}

Point& Point::operator-=(const Point& r)
{
x -= r.x;
y -= r.y;
return *this;
}

Point& Point::operator/=(int downscale)
{
x /= downscale;
Expand Down Expand Up @@ -606,12 +469,6 @@ void Bitmap::fill()
}
}

void Bitmap::fillWithStone()
{
StoneTextureGenerator gen;
gen.generateStoneTexture(*this);
}

void Bitmap::copyBorders(const Bitmap& src, Size cornerSize)
{
makeBorderImage(*this, src, cornerSize);
Expand All @@ -626,16 +483,63 @@ void Bitmap::draw(Point destPoint, const Bitmap& src, Point srcPoint, Size size)
}
}

void Bitmap::tileWith(const Bitmap& tile)
void Bitmap::drawBlended(Point destPoint, const Bitmap& src, Point srcPoint, Size size)
{
const int w = tile.size.width - 1;
const int h = tile.size.height - 1;
for (int x = 0; x < size.width; x++) {
const int srcX = x % w;
for (int y = 0; y < size.height; y++) {
const int srcY = y % h;
for (int y = 0; y < size.height; ++y) {
for (int x = 0; x < size.width; ++x) {
auto* destPixel = getPixel(destPoint + Point{ x, y });
const auto* srcPixel = src.getPixel(srcPoint + Point{ x, y });
if (!destPixel || !srcPixel)
continue;

if (destPixel[3] == 0) {
memcpy(destPixel, srcPixel, 4);
continue;
}

drawDot({ x, y }, tile, { srcX, srcY });
const int alpha = srcPixel[3];
for (int channel = 0; channel < 3; ++channel)
destPixel[channel] = uint8_t((srcPixel[channel] * alpha + destPixel[channel] * (255 - alpha)) / 255);
if (alpha > destPixel[3])
destPixel[3] = uint8_t(alpha);
}
}
}

void Bitmap::drawScaled(Rect destRect, const Bitmap& src, Rect srcRect)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is something we should avoid, all stuff in json have like "scale: 1.2" and stuff, so at least no rescaling, I wish we can remove blender later, but rescaled is suspicios

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha. Let me think of another way to fix it. Originally I use draw, then realized, for some reasons, the MKB panel is actually larger than the cropped controller panel.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

he MKB panel is actually larger than the cropped controller panel.
yeah, tile still should work. At worst we can resort and mirror and copy (flip image vertically) to not see horizontal tile repeat. but vertical tiling must be possible seamless with this texture.

{
if (destRect.size.width <= 0 || destRect.size.height <= 0 || srcRect.size.width <= 0 || srcRect.size.height <= 0)
return;

const double scaleX = double(srcRect.size.width) / destRect.size.width;
const double scaleY = double(srcRect.size.height) / destRect.size.height;
const int maxX = srcRect.size.width - 1;
const int maxY = srcRect.size.height - 1;

for (int y = 0; y < destRect.size.height; ++y) {
const double sy = std::clamp((y + 0.5) * scaleY - 0.5, 0.0, double(maxY));
const int y0 = int(sy);
const int y1 = std::min(y0 + 1, maxY);
const double fy = sy - y0;
for (int x = 0; x < destRect.size.width; ++x) {
const double sx = std::clamp((x + 0.5) * scaleX - 0.5, 0.0, double(maxX));
const int x0 = int(sx);
const int x1 = std::min(x0 + 1, maxX);
const double fx = sx - x0;

auto* destPixel = getPixel(destRect.topLeft + Point{ x, y });
const uint8_t* p00 = src.getPixel(srcRect.topLeft + Point{ x0, y0 });
const uint8_t* p10 = src.getPixel(srcRect.topLeft + Point{ x1, y0 });
const uint8_t* p01 = src.getPixel(srcRect.topLeft + Point{ x0, y1 });
const uint8_t* p11 = src.getPixel(srcRect.topLeft + Point{ x1, y1 });
if (!destPixel || !p00 || !p10 || !p01 || !p11)
continue;

for (int channel = 0; channel < 4; ++channel) {
const double top = p00[channel] + (p10[channel] - p00[channel]) * fx;
const double bottom = p01[channel] + (p11[channel] - p01[channel]) * fx;
destPixel[channel] = uint8_t(std::lround(top + (bottom - top) * fy));
}
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/CoreLib/Image.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ struct Point {
void read(const Mernel::PropertyTree& data);
void write(Mernel::PropertyTree& data) const;
Point operator+(const Point& s);
Point operator-(const Point& s);

Point& operator+=(const Point& r);
Point& operator-=(const Point& r);
Point& operator/=(int downscale);
std::string toString() const;
};
Expand Down Expand Up @@ -150,15 +152,15 @@ struct MODGENCORE_EXPORT Bitmap {
void paintLineV(Point point, int height);

void fill();
void fillWithStone();
void copyBorders(const Bitmap& src, Size cornerSize);

void fillRect(Rect rect);
void linedRect(Rect rect);

void draw(Point destPoint, const Bitmap& src, Point srcPoint, Size size);
void drawBlended(Point destPoint, const Bitmap& src, Point srcPoint, Size size);

void tileWith(const Bitmap& tile);
void drawScaled(Rect destRect, const Bitmap& src, Rect srcRect); // bilinear

void drawDot(Point destPoint, const Bitmap& src, Point srcPoint);
};
Expand Down
Loading
Loading