mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Fix character texture compositing and add YouTube video to README
Use authoritative WoW Model Viewer atlas coordinates (256x256 reference) for face, underwear, and body region placement. Previously all coordinates were hardcoded at 512x512 scale causing overlays to be clipped on the 256x256 base skin. Also include face upper+lower textures from CharSections.dbc in compositing for both player and other-player characters.
This commit is contained in:
parent
7f0eceaacc
commit
9bc8c5c85a
3 changed files with 114 additions and 57 deletions
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
A native C++ World of Warcraft client with a custom OpenGL renderer.
|
A native C++ World of Warcraft client with a custom OpenGL renderer.
|
||||||
|
|
||||||
|
[](https://youtu.be/Pd9JuYYxu0o)
|
||||||
|
|
||||||
Primary target today is **WotLK 3.3.5a**, with active work to broaden compatibility across **Vanilla (Classic) + TBC + WotLK**.
|
Primary target today is **WotLK 3.3.5a**, with active work to broaden compatibility across **Vanilla (Classic) + TBC + WotLK**.
|
||||||
|
|
||||||
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.
|
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.
|
||||||
|
|
|
||||||
|
|
@ -1807,6 +1807,7 @@ void Application::spawnPlayerCharacter() {
|
||||||
std::string bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp";
|
std::string bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp";
|
||||||
std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp";
|
std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp";
|
||||||
std::string faceLowerTexturePath;
|
std::string faceLowerTexturePath;
|
||||||
|
std::string faceUpperTexturePath;
|
||||||
std::vector<std::string> underwearPaths;
|
std::vector<std::string> underwearPaths;
|
||||||
|
|
||||||
// Extract appearance bytes for texture lookups
|
// Extract appearance bytes for texture lookups
|
||||||
|
|
@ -1862,15 +1863,21 @@ void Application::spawnPlayerCharacter() {
|
||||||
" (style=", (int)charHairStyleId, " color=", (int)charHairColorId, ")");
|
" (style=", (int)charHairStyleId, " color=", (int)charHairColorId, ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Section 1 = face lower: match variation=faceId
|
// Section 1 = face: match variation=faceId, colorIndex=skinId
|
||||||
|
// Texture1 = face lower, Texture2 = face upper
|
||||||
else if (baseSection == 1 && !foundFaceLower &&
|
else if (baseSection == 1 && !foundFaceLower &&
|
||||||
variationIndex == charFaceId && colorIndex == charSkinId) {
|
variationIndex == charFaceId && colorIndex == charSkinId) {
|
||||||
std::string tex1 = charSectionsDbc->getString(r, csTex1);
|
std::string tex1 = charSectionsDbc->getString(r, csTex1);
|
||||||
|
std::string tex2 = charSectionsDbc->getString(r, csTex1 + 1);
|
||||||
if (!tex1.empty()) {
|
if (!tex1.empty()) {
|
||||||
faceLowerTexturePath = tex1;
|
faceLowerTexturePath = tex1;
|
||||||
foundFaceLower = true;
|
LOG_INFO(" DBC face lower: ", faceLowerTexturePath);
|
||||||
LOG_INFO(" DBC face texture: ", faceLowerTexturePath);
|
|
||||||
}
|
}
|
||||||
|
if (!tex2.empty()) {
|
||||||
|
faceUpperTexturePath = tex2;
|
||||||
|
LOG_INFO(" DBC face upper: ", faceUpperTexturePath);
|
||||||
|
}
|
||||||
|
foundFaceLower = true;
|
||||||
}
|
}
|
||||||
// Section 4 = underwear
|
// Section 4 = underwear
|
||||||
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
|
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
|
||||||
|
|
@ -1943,21 +1950,25 @@ void Application::spawnPlayerCharacter() {
|
||||||
bodySkinPath_ = bodySkinPath;
|
bodySkinPath_ = bodySkinPath;
|
||||||
underwearPaths_ = underwearPaths;
|
underwearPaths_ = underwearPaths;
|
||||||
|
|
||||||
// Composite body skin + underwear overlays
|
// Composite body skin + face + underwear overlays
|
||||||
if (!underwearPaths.empty()) {
|
{
|
||||||
std::vector<std::string> layers;
|
std::vector<std::string> layers;
|
||||||
layers.push_back(bodySkinPath);
|
layers.push_back(bodySkinPath);
|
||||||
|
if (!faceLowerTexturePath.empty()) layers.push_back(faceLowerTexturePath);
|
||||||
|
if (!faceUpperTexturePath.empty()) layers.push_back(faceUpperTexturePath);
|
||||||
for (const auto& up : underwearPaths) {
|
for (const auto& up : underwearPaths) {
|
||||||
layers.push_back(up);
|
layers.push_back(up);
|
||||||
}
|
}
|
||||||
GLuint compositeTex = charRenderer->compositeTextures(layers);
|
if (layers.size() > 1) {
|
||||||
if (compositeTex != 0) {
|
GLuint compositeTex = charRenderer->compositeTextures(layers);
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
if (compositeTex != 0) {
|
||||||
if (model.textures[ti].type == 1) {
|
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
||||||
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), compositeTex);
|
if (model.textures[ti].type == 1) {
|
||||||
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), compositeTex);
|
||||||
LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+underwear");
|
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
||||||
break;
|
LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+face+underwear");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3727,6 +3738,8 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp";
|
std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp";
|
||||||
std::vector<std::string> underwearPaths;
|
std::vector<std::string> underwearPaths;
|
||||||
std::string hairTexturePath;
|
std::string hairTexturePath;
|
||||||
|
std::string faceLowerPath;
|
||||||
|
std::string faceUpperPath;
|
||||||
|
|
||||||
uint8_t skinId = appearanceBytes & 0xFF;
|
uint8_t skinId = appearanceBytes & 0xFF;
|
||||||
uint8_t faceId = (appearanceBytes >> 8) & 0xFF;
|
uint8_t faceId = (appearanceBytes >> 8) & 0xFF;
|
||||||
|
|
@ -3743,7 +3756,6 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
bool foundUnderwear = false;
|
bool foundUnderwear = false;
|
||||||
bool foundHair = false;
|
bool foundHair = false;
|
||||||
bool foundFaceLower = false;
|
bool foundFaceLower = false;
|
||||||
(void)faceId; // face lower not yet applied as separate layer
|
|
||||||
|
|
||||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||||
uint32_t rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
uint32_t rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||||
|
|
@ -3769,6 +3781,10 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
foundUnderwear = true;
|
foundUnderwear = true;
|
||||||
} else if (baseSection == 1 && !foundFaceLower &&
|
} else if (baseSection == 1 && !foundFaceLower &&
|
||||||
variationIndex == faceId && colorIndex == skinId) {
|
variationIndex == faceId && colorIndex == skinId) {
|
||||||
|
std::string tex1 = charSectionsDbc->getString(r, csTex1);
|
||||||
|
std::string tex2 = charSectionsDbc->getString(r, csTex1 + 1);
|
||||||
|
if (!tex1.empty()) faceLowerPath = tex1;
|
||||||
|
if (!tex2.empty()) faceUpperPath = tex2;
|
||||||
foundFaceLower = true;
|
foundFaceLower = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3776,15 +3792,19 @@ void Application::spawnOnlinePlayer(uint64_t guid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Composite base skin + underwear overlays (same as local character logic)
|
// Composite base skin + face + underwear overlays
|
||||||
GLuint compositeTex = 0;
|
GLuint compositeTex = 0;
|
||||||
if (!underwearPaths.empty()) {
|
{
|
||||||
std::vector<std::string> layers;
|
std::vector<std::string> layers;
|
||||||
layers.push_back(bodySkinPath);
|
layers.push_back(bodySkinPath);
|
||||||
|
if (!faceLowerPath.empty()) layers.push_back(faceLowerPath);
|
||||||
|
if (!faceUpperPath.empty()) layers.push_back(faceUpperPath);
|
||||||
for (const auto& up : underwearPaths) layers.push_back(up);
|
for (const auto& up : underwearPaths) layers.push_back(up);
|
||||||
compositeTex = charRenderer->compositeTextures(layers);
|
if (layers.size() > 1) {
|
||||||
} else {
|
compositeTex = charRenderer->compositeTextures(layers);
|
||||||
compositeTex = charRenderer->loadTexture(bodySkinPath);
|
} else {
|
||||||
|
compositeTex = charRenderer->loadTexture(bodySkinPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint hairTex = 0;
|
GLuint hairTex = 0;
|
||||||
|
|
|
||||||
|
|
@ -483,6 +483,21 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
|
||||||
|
|
||||||
core::Logger::getInstance().info("Composite: base layer ", width, "x", height, " from ", layerPaths[0]);
|
core::Logger::getInstance().info("Composite: base layer ", width, "x", height, " from ", layerPaths[0]);
|
||||||
|
|
||||||
|
// WoW character texture atlas regions (from WoW Model Viewer / CharComponentTextureSections)
|
||||||
|
// Coordinates at 256x256 base resolution:
|
||||||
|
// Region X Y W H
|
||||||
|
// Base 0 0 256 256
|
||||||
|
// Arm Upper 0 0 128 64
|
||||||
|
// Arm Lower 0 64 128 64
|
||||||
|
// Hand 0 128 128 32
|
||||||
|
// Face Upper 0 160 128 32
|
||||||
|
// Face Lower 0 192 128 64
|
||||||
|
// Torso Upper 128 0 128 64
|
||||||
|
// Torso Lower 128 64 128 32
|
||||||
|
// Pelvis Upper 128 96 128 64
|
||||||
|
// Pelvis Lower 128 160 128 64
|
||||||
|
// Foot 128 224 128 32
|
||||||
|
|
||||||
// Alpha-blend each overlay onto the composite
|
// Alpha-blend each overlay onto the composite
|
||||||
for (size_t layer = 1; layer < layerPaths.size(); layer++) {
|
for (size_t layer = 1; layer < layerPaths.size(); layer++) {
|
||||||
if (layerPaths[layer].empty()) continue;
|
if (layerPaths[layer].empty()) continue;
|
||||||
|
|
@ -500,58 +515,62 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
|
||||||
// Same size: full alpha-blend
|
// Same size: full alpha-blend
|
||||||
blitOverlay(composite, width, height, overlay, 0, 0);
|
blitOverlay(composite, width, height, overlay, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
// WoW character texture layout (512x512, from CharComponentTextureSections):
|
|
||||||
// Region X Y W H
|
|
||||||
// 0 Base 0 0 512 512
|
|
||||||
// 1 Arm Upper 0 0 256 128
|
|
||||||
// 2 Arm Lower 0 128 256 128
|
|
||||||
// 3 Hand 0 256 256 64
|
|
||||||
// 4 Face Upper 0 320 256 64
|
|
||||||
// 5 Face Lower 0 384 256 128
|
|
||||||
// 6 Torso Upper 256 0 256 128
|
|
||||||
// 7 Torso Lower 256 128 256 64
|
|
||||||
// 8 Pelvis Upper 256 192 256 128
|
|
||||||
// 9 Pelvis Lower 256 320 256 128
|
|
||||||
// 10 Foot 256 448 256 64
|
|
||||||
//
|
|
||||||
// Determine region by filename keywords
|
// Determine region by filename keywords
|
||||||
|
// Coordinates scale with base texture size (256x256 is reference)
|
||||||
|
float s = width / 256.0f;
|
||||||
int dstX = 0, dstY = 0;
|
int dstX = 0, dstY = 0;
|
||||||
std::string pathLower = layerPaths[layer];
|
std::string pathLower = layerPaths[layer];
|
||||||
for (auto& c : pathLower) c = std::tolower(c);
|
for (auto& c : pathLower) c = std::tolower(c);
|
||||||
|
|
||||||
if (pathLower.find("pelvis") != std::string::npos) {
|
if (pathLower.find("faceupper") != std::string::npos) {
|
||||||
// Pelvis Upper: (256, 192) 256x128
|
dstX = 0; dstY = static_cast<int>(160 * s);
|
||||||
dstX = 256;
|
} else if (pathLower.find("facelower") != std::string::npos) {
|
||||||
dstY = 192;
|
dstX = 0; dstY = static_cast<int>(192 * s);
|
||||||
core::Logger::getInstance().info("Composite: placing pelvis region at (", dstX, ",", dstY, ")");
|
} else if (pathLower.find("pelvis") != std::string::npos) {
|
||||||
|
dstX = static_cast<int>(128 * s);
|
||||||
|
dstY = static_cast<int>(96 * s);
|
||||||
} else if (pathLower.find("torso") != std::string::npos) {
|
} else if (pathLower.find("torso") != std::string::npos) {
|
||||||
// Torso Upper: (256, 0) 256x128
|
dstX = static_cast<int>(128 * s);
|
||||||
dstX = 256;
|
|
||||||
dstY = 0;
|
dstY = 0;
|
||||||
core::Logger::getInstance().info("Composite: placing torso region at (", dstX, ",", dstY, ")");
|
|
||||||
} else if (pathLower.find("armupper") != std::string::npos) {
|
} else if (pathLower.find("armupper") != std::string::npos) {
|
||||||
dstX = 0; dstY = 0;
|
dstX = 0; dstY = 0;
|
||||||
} else if (pathLower.find("armlower") != std::string::npos) {
|
} else if (pathLower.find("armlower") != std::string::npos) {
|
||||||
dstX = 0; dstY = 128;
|
dstX = 0; dstY = static_cast<int>(64 * s);
|
||||||
} else if (pathLower.find("hand") != std::string::npos) {
|
} else if (pathLower.find("hand") != std::string::npos) {
|
||||||
dstX = 0; dstY = 256;
|
dstX = 0; dstY = static_cast<int>(128 * s);
|
||||||
} else if (pathLower.find("foot") != std::string::npos || pathLower.find("feet") != std::string::npos) {
|
} else if (pathLower.find("foot") != std::string::npos || pathLower.find("feet") != std::string::npos) {
|
||||||
dstX = 256; dstY = 448;
|
dstX = static_cast<int>(128 * s); dstY = static_cast<int>(224 * s);
|
||||||
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
|
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
|
||||||
dstX = 256; dstY = 320;
|
dstX = static_cast<int>(128 * s); dstY = static_cast<int>(160 * s);
|
||||||
} else {
|
} else {
|
||||||
// Unknown — center placement as fallback
|
// Unknown — center placement as fallback
|
||||||
dstX = (width - overlay.width) / 2;
|
dstX = (width - overlay.width) / 2;
|
||||||
dstY = (height - overlay.height) / 2;
|
dstY = (height - overlay.height) / 2;
|
||||||
core::Logger::getInstance().info("Composite: unknown region '", layerPaths[layer], "', placing at (", dstX, ",", dstY, ")");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core::Logger::getInstance().info("Composite: placing '", layerPaths[layer], "' at (", dstX, ",", dstY, ") on ", width, "x", height);
|
||||||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug dump removed: it was always-on and could stall badly under load.
|
// Debug dump removed: it was always-on and could stall badly under load.
|
||||||
|
|
||||||
|
// Debug: dump first composite to /tmp for visual inspection
|
||||||
|
{
|
||||||
|
static bool dumped = false;
|
||||||
|
if (!dumped && layerPaths.size() > 1) {
|
||||||
|
dumped = true;
|
||||||
|
std::string dumpPath = "/tmp/wowee_composite_debug.raw";
|
||||||
|
FILE* f = fopen(dumpPath.c_str(), "wb");
|
||||||
|
if (f) {
|
||||||
|
fwrite(composite.data(), 1, composite.size(), f);
|
||||||
|
fclose(f);
|
||||||
|
core::Logger::getInstance().info("DEBUG: dumped composite ", width, "x", height,
|
||||||
|
" RGBA to ", dumpPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upload composite to GPU
|
// Upload composite to GPU
|
||||||
GLuint texId;
|
GLuint texId;
|
||||||
glGenTextures(1, &texId);
|
glGenTextures(1, &texId);
|
||||||
|
|
@ -643,7 +662,10 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
composite = base.data;
|
composite = base.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blend underwear overlays (same logic as compositeTextures)
|
// Blend face + underwear overlays
|
||||||
|
// These are native-resolution textures (designed for 256x256 base).
|
||||||
|
// If we upscaled the base to 512x512, use blitOverlayScaled2x and 2x coords.
|
||||||
|
bool upscaled = (base.width == 256 && base.height == 256 && width == 512);
|
||||||
for (const auto& ul : baseLayers) {
|
for (const auto& ul : baseLayers) {
|
||||||
if (ul.empty()) continue;
|
if (ul.empty()) continue;
|
||||||
auto overlay = assetManager->loadTexture(ul);
|
auto overlay = assetManager->loadTexture(ul);
|
||||||
|
|
@ -652,29 +674,42 @@ GLuint CharacterRenderer::compositeWithRegions(const std::string& basePath,
|
||||||
if (overlay.width == width && overlay.height == height) {
|
if (overlay.width == width && overlay.height == height) {
|
||||||
blitOverlay(composite, width, height, overlay, 0, 0);
|
blitOverlay(composite, width, height, overlay, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
|
// WoW 256-scale atlas coordinates (from CharComponentTextureSections)
|
||||||
int dstX = 0, dstY = 0;
|
int dstX = 0, dstY = 0;
|
||||||
std::string pathLower = ul;
|
std::string pathLower = ul;
|
||||||
for (auto& c : pathLower) c = std::tolower(c);
|
for (auto& c : pathLower) c = std::tolower(c);
|
||||||
|
|
||||||
if (pathLower.find("pelvis") != std::string::npos) {
|
if (pathLower.find("faceupper") != std::string::npos) {
|
||||||
dstX = 256; dstY = 192;
|
dstX = 0; dstY = 160;
|
||||||
|
} else if (pathLower.find("facelower") != std::string::npos) {
|
||||||
|
dstX = 0; dstY = 192;
|
||||||
|
} else if (pathLower.find("pelvis") != std::string::npos) {
|
||||||
|
dstX = 128; dstY = 96;
|
||||||
} else if (pathLower.find("torso") != std::string::npos) {
|
} else if (pathLower.find("torso") != std::string::npos) {
|
||||||
dstX = 256; dstY = 0;
|
dstX = 128; dstY = 0;
|
||||||
} else if (pathLower.find("armupper") != std::string::npos) {
|
} else if (pathLower.find("armupper") != std::string::npos) {
|
||||||
dstX = 0; dstY = 0;
|
dstX = 0; dstY = 0;
|
||||||
} else if (pathLower.find("armlower") != std::string::npos) {
|
} else if (pathLower.find("armlower") != std::string::npos) {
|
||||||
dstX = 0; dstY = 128;
|
dstX = 0; dstY = 64;
|
||||||
} else if (pathLower.find("hand") != std::string::npos) {
|
} else if (pathLower.find("hand") != std::string::npos) {
|
||||||
dstX = 0; dstY = 256;
|
dstX = 0; dstY = 128;
|
||||||
} else if (pathLower.find("foot") != std::string::npos || pathLower.find("feet") != std::string::npos) {
|
} else if (pathLower.find("foot") != std::string::npos || pathLower.find("feet") != std::string::npos) {
|
||||||
dstX = 256; dstY = 448;
|
dstX = 128; dstY = 224;
|
||||||
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
|
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
|
||||||
dstX = 256; dstY = 320;
|
dstX = 128; dstY = 160;
|
||||||
} else {
|
} else {
|
||||||
dstX = (width - overlay.width) / 2;
|
dstX = (base.width - overlay.width) / 2;
|
||||||
dstY = (height - overlay.height) / 2;
|
dstY = (base.height - overlay.height) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upscaled) {
|
||||||
|
// Scale coords and texels to match 512x512 canvas
|
||||||
|
dstX *= 2;
|
||||||
|
dstY *= 2;
|
||||||
|
blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY);
|
||||||
|
} else {
|
||||||
|
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
||||||
}
|
}
|
||||||
blitOverlay(composite, width, height, overlay, dstX, dstY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue