Глава 29. Chroma key

Chroma keying, понятно, это способ считать какой-то конкретный цвет прозрачным. В нашем случае – зеленый:

Конечно, после загрузки изображения, перед тем как сделать из него текстуру, мы можем легко просканировать его на предмет зеленых пикселей, и установить их Альфа-каналы в ноль. Правда, когда GL rasterizer выбирает tUV координаты рядом с зеленой зоной, результирующий пиксель на экране будет частично “прозрачным” и частичнозеленым. Кстати, безотносительно прозрачности. В нашем примере это проявляется как зеленоватые тонкие линии по границам проекций:

Да, это важно, какие цвета окружают ваши изображения.

Решение: при сканировании на предмет зеленых пикселей, можно установить их Альфа-каналы в ноль, И также их RGB компоненты – в цвета ближайших непрозрачных пикселей. Чтобы результирующее изображение было:

  • Бывшие зеленые области теперь прозрачные, плюс их RGB компоненты больше не повлияют на соседние пиксели.

Реализация:

1. Запускаем VS, открываем C:\CPP\a997modeler\p_windows\p_windows.sln.


В классе Texture у нас новая функция, applyCkey(..), и новый параметр в loadTexture(..).

2. Заменим Texture.h код на:

#pragma once
#include <string>
#include <vector>
#include "platform.h"

class Texture
{
public:
    //texture's individual descriptor:
    unsigned int GLid = -1; // GL texture id
    int size[2] = { 0,0 };  // image size
    std::string source; //file name
    //if renderable ?
    unsigned int frameBufferId = 0;
    unsigned int depthBufferId = 0;
    //end of descriptor

    //static array (vector) of all loaded textures
    static std::vector<Texture*> textures;
public:
    static int loadTexture(std::string filePath, unsigned int ckey, int glRepeatH = GL_MIRRORED_REPEAT, int glRepeatV = GL_MIRRORED_REPEAT);
    static int applyCkey(unsigned char* imgData, int w, int h, unsigned int ckey);
    static int findTexture(std::string filePath);
    static int cleanUp();
    static unsigned int getGLid(int texN) { return textures.at(texN)->GLid; };
    static int saveBMP(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel=4);
    static int saveTGA(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel=4);
    static int generateTexture(std::string imgID, int w, int h, unsigned char* imgData, int glRepeatH = GL_MIRRORED_REPEAT, int glRepeatV = GL_MIRRORED_REPEAT);
    static int detachRenderBuffer(Texture* pTex);
    static int attachRenderBuffer(int texN, bool zBuffer = false) { return attachRenderBuffer(textures.at(texN), zBuffer); };
    static int attachRenderBuffer(Texture* pTex, bool zBuffer = false);
    static int setRenderToTexture(int texN) { return setRenderToTexture(textures.at(texN)); };
    static int setRenderToTexture(Texture* pTex);
    static int getImageFromTexture(int texN, unsigned char* imgData);
    static int blurRGBA(unsigned char* imgData, int w, int h, int blurLevel);
};


3. Заменим Texture.cpp код на:

#include "Texture.h"
#define STB_IMAGE_IMPLEMENTATION  //required by stb_image.h
#include "stb_image.h"
#include "platform.h"
#include "utils.h"
#include "MyColor.h"

//static array (vector) of all loaded textures
std::vector<Texture*> Texture::textures;

int Texture::loadTexture(std::string filePath, unsigned int ckey, int glRepeatH, int glRepeatV) {
    int texN = findTexture(filePath);
    if (texN >= 0)
        return texN;
    //if here - texture wasn't loaded
    // load an image
    int nrChannels, w, h;
    unsigned char* imgData = stbi_load(filePath.c_str(),
        &w, &h, &nrChannels, 4); //"4"-convert to 4 channels -RGBA
    if (imgData == NULL) {
        mylog("ERROR in Texture::loadTexture loading image %s\n", filePath.c_str());
    }
    if (ckey != 0)
        applyCkey(imgData, w, h, ckey);
    // generate texture
    generateTexture(filePath, w, h, imgData, glRepeatH, glRepeatV);
    // release image data
    stbi_image_free(imgData);

    return (textures.size() - 1);
}
int Texture::findTexture(std::string filePath) {
    int texturesN = textures.size();
    if (texturesN < 1)
        return -1;
    for (int i = 0; i < texturesN; i++) {
        Texture* pTex = textures.at(i);
        if (pTex->source.compare(filePath) == 0)
            return i;
    }
    return -1;
}
int Texture::cleanUp() {
    int texturesN = textures.size();
    if (texturesN < 1)
        return -1;
    //detach all textures
    glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture
    glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, 0);
    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D, 0);
    //release all textures
    for (int i = 0; i < texturesN; i++) {
        Texture* pTex = textures.at(i);
        detachRenderBuffer(pTex);
        glDeleteTextures(1, (GLuint*)&pTex->GLid);
        pTex->GLid = 0;
        delete pTex;
    }
    textures.clear();
    return 1;
}
int Texture::saveBMP(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel) {
    std::string fullPath = getFullPath(filePath);
    std::string inAppPath = getInAppPath(fullPath);
    makeDirs(inAppPath);
    FILE* outFile;
    myFopen_s(&outFile, fullPath.c_str(), "wb");
    if (outFile == NULL) {
        mylog("ERROR in Texture::saveBMP: Can't create file %s\n", filePath.c_str());
        return -1;
    }
    struct {
        char chars2skip[2]; //
            //BMP Header
        char bm[2] = { 0x42, 0x4D }; //	"BM"
        myUint32 fileSize = 0; // Size of the BMP file, little-endian
        myUint32 unused = 0;
        myUint32 dataOffset = 0; // Offset where the pixel array (bitmap data) can be found, little-endian
        //DIB Header
        myUint32 dibHeaderSize = 0; // Number of bytes in the DIB header, little-endian
        myUint32 imgW = 0; // Width of the bitmap in pixels, little-endian
        myUint32 imgH = 0; // Height of the bitmap in pixels, little-endian
        char colorPlainsN[2] = { 1,0 };
        char bitsPerPixel[2] = { 32,0 };
        myUint32 compression = 0; //0-BI_RGB
        myUint32 dataSize = 0; // Size of the raw bitmap data (including padding), little-endian
        myUint32 printResution[2] = { 2835 ,2835 }; // Print resolution of the image,
                //72 DPI × 39.3701 inches per metre yields 2834.6472, little-endian
        myUint32 paletteColors = 0; // Number of colors in the palette
        myUint32 importantColors = 0; //0 means all colors are important
    } bmpHeader;
    int rowSize = w * bytesPerPixel;
    int rowPadding = (4 - rowSize % 4) % 4;
    int rowSizeWithPadding = rowSize + rowPadding;
    int dataSize = rowSizeWithPadding * h;
    int headerSize = sizeof(bmpHeader) - 2; //-chars2skip
    bmpHeader.fileSize = dataSize + headerSize;
    bmpHeader.dataOffset = headerSize;
    bmpHeader.dibHeaderSize = headerSize - 14; //-BMP Header size
    bmpHeader.imgW = w;
    bmpHeader.imgH = h;
    if (bytesPerPixel != 4)
        bmpHeader.bitsPerPixel[0] = bytesPerPixel * 8;
    bmpHeader.dataSize = dataSize;
    fwrite(&bmpHeader.bm, 1, headerSize, outFile);
    //data, from bottom to top
    unsigned char zero[4] = { 0,0,0,0 };
    unsigned char bgra[4];
    for (int y = h - 1; y >= 0; y--) {
        for (int x = 0; x < w; x++) {
            int pixelOffset = y * rowSize + x * 4;
            bgra[0] = buff[pixelOffset + 2];
            bgra[1] = buff[pixelOffset + 1];
            bgra[2] = buff[pixelOffset + 0];
            bgra[3] = buff[pixelOffset + 3];
            fwrite(bgra, 1, bytesPerPixel, outFile);
        }
        if (rowPadding != 0)
            fwrite(zero, 1, rowPadding, outFile);
    }
    fflush(outFile);
    fclose(outFile);

    return 1;
}

int Texture::saveTGA(std::string filePath, unsigned char* buff, int w, int h, int bytesPerPixel) {
    std::string fullPath = getFullPath(filePath);
    std::string inAppPath = getInAppPath(fullPath);
    makeDirs(inAppPath);
    FILE* outFile;
    myFopen_s(&outFile, fullPath.c_str(), "wb");
    if (outFile == NULL) {
        mylog("ERROR in Texture::saveBMP: Can't create file %s\n", filePath.c_str());
        return -1;
    }
    unsigned char tgaHeader[18] = { 0,0,2,0,0,0,0,0,0,0,0,0, (unsigned char)(w % 256), (unsigned char)(w / 256),
        (unsigned char)(h % 256), (unsigned char)(h / 256), (unsigned char)(bytesPerPixel * 8), 0x20 };
    fwrite(tgaHeader, 1, 18, outFile);
    //data
    unsigned char bgra[4];
    for (int i = 0; i < w * h; i++) {
        int pixelOffset = i * 4;
        bgra[0] = buff[pixelOffset + 2];
        bgra[1] = buff[pixelOffset + 1];
        bgra[2] = buff[pixelOffset + 0];
        bgra[3] = buff[pixelOffset + 3];
        fwrite(bgra, 1, bytesPerPixel, outFile);
    }
    fflush(outFile);
    fclose(outFile);

    return 1;
}
int Texture::generateTexture(std::string imgID, int w, int h, unsigned char* imgData, int glRepeatH, int glRepeatV) {
    //glRepeat options: GL_REPEAT, GL_CLAMP_TO_EDGE, GL_MIRRORED_REPEAT
    if (!imgID.empty()) {
        int texN = findTexture(imgID);
        if (texN >= 0)
            return texN;
    }
    //if here - texture wasn't generated
    //create Texture object
    Texture* pTex = new Texture();
    textures.push_back(pTex);
    pTex->size[0] = w;
    pTex->size[1] = h;
    pTex->source.assign(imgID);
    // generate texture
    glGenTextures(1, (GLuint*)&pTex->GLid);
    glBindTexture(GL_TEXTURE_2D, pTex->GLid);
    // set the texture wrapping/filtering options (on the currently bound texture object)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, glRepeatH);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, glRepeatV);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);// GL_LINEAR);  //
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // attach image data (if provided)
    if (imgData != NULL) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pTex->size[0], pTex->size[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, imgData);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    return (textures.size() - 1);
}
int Texture::detachRenderBuffer(Texture* pTex) {
    if (pTex->frameBufferId == 0)
        return 0;
    if (pTex->depthBufferId > 0) {
        glDeleteRenderbuffers(1, (GLuint*)&pTex->depthBufferId);
        pTex->depthBufferId = 0;
    }
    glDeleteFramebuffers(1, (GLuint*)&pTex->frameBufferId);
    pTex->frameBufferId = 0;
    return 1;
}

int Texture::attachRenderBuffer(Texture* pTex, bool zBuffer) {
    if (pTex->frameBufferId > 0)
        return 0; //attached already
    //generate frame buffer
    glGenFramebuffers(1, (GLuint*)&pTex->frameBufferId);
    if (zBuffer) {
        //generate depth buffer
        glGenRenderbuffers(1, (GLuint*)&pTex->depthBufferId);
        // create render buffer and bind 16-bit depth buffer
        glBindRenderbuffer(GL_RENDERBUFFER, pTex->depthBufferId);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, pTex->size[0], pTex->size[1]);
        glBindRenderbuffer(GL_RENDERBUFFER, 0); //release
    }
    return 1;
}
int Texture::setRenderToTexture(Texture* pTex) {
    if (pTex->frameBufferId == 0) {
        mylog("ERROR in Texture::setRenderToTexture: %s not renderable", pTex->source.c_str());
        return -1;
    }
    // Bind the framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, pTex->frameBufferId);
    // specify texture as color attachment
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pTex->GLid, 0);
    // attach render buffer as depth buffer
    if (pTex->depthBufferId > 0) {
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, pTex->depthBufferId);
        glClear(GL_DEPTH_BUFFER_BIT);
    }
    // check status
    int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        std::string str;
        if (status == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT)
            str.assign("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
        else if (status == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT)
            str.assign("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
        else if (status == GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE)
            str.assign("GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE");
        else if (status == GL_FRAMEBUFFER_UNSUPPORTED)
            str.assign("GL_FRAMEBUFFER_UNSUPPORTED");
        else
            str.assign("hz");
        mylog("Modeler.setRenderToTextureBind to texture %s failed: %s\n", pTex->source.c_str(), str.c_str());
        return -1;
    }
    glViewport(0, 0, pTex->size[0], pTex->size[1]);
    return 1;
}
int Texture::getImageFromTexture(int texN, unsigned char* imgData) {
    Texture* pTex = textures.at(texN);
    glBindTexture(GL_TEXTURE_2D, pTex->GLid);
    glBindFramebuffer(GL_FRAMEBUFFER, pTex->frameBufferId);

    glReadPixels(0, 0, pTex->size[0], pTex->size[1], GL_RGBA, GL_UNSIGNED_BYTE, imgData);
    return 1;
}
int Texture::blurRGBA(unsigned char* imgData, int w0, int h0, int blurLevel) {
    unsigned char* imgTemp = new unsigned char[w0 * h0 * 4];
    int w00 = blurLevel * 2 + 1;
    for (int y0 = 0; y0 < h0; y0++) {
        int y1 = y0 - blurLevel;
        int h1 = w00;
        if (y1 < 0) {
            int d = -y1;
            y1 += d;
            h1 -= d;
        }
        else if (y1 > h0 - w00) {
            int d = y1 - (h0 - w00);
            h1 -= d;
        }
        for (int x0 = 0; x0 < w0; x0++) {
            int x1 = x0 - blurLevel;
            int w1 = w00;
            if (x1 < 0) {
                int d = -x1;
                x1 += d;
                w1 -= d;
            }
            else if (x1 > w0 - w00) {
                int d = x1 - (w0 - w00);
                w1 -= d;
            }
            int sum[4] = { 0,0,0,0 };
            for (int y = y1; y < y1 + h1; y++) {
                for (int x = x1; x < x1 + w1; x++) {
                    int idx = (y * w0 + x) * 4;
                    for (int ch = 0; ch < 4; ch++)
                        sum[ch] += imgData[idx + ch];
                }
            }
            int n = w1 * h1;
            int idx = (y0 * w0 + x0) * 4;
            for (int ch = 0; ch < 4; ch++)
                imgTemp[idx + ch] = (unsigned char)(sum[ch] / n);
        }
    }
    memcpy(imgData, imgTemp, w0 * h0 * 4);
    delete[] imgTemp;
    return 1;
}
int Texture::applyCkey(unsigned char* imgData, int w0, int h0, unsigned int ckey) {
    if (ckey == 0)
        return 0;
    MyColor transparentPixel;
    transparentPixel.setRGBA(127, 127, 127, 0);
    unsigned int transparentValue = transparentPixel.getUint32();
    unsigned int* pIntData = (unsigned int*)imgData;
    int dataIntsN = w0 * h0;
    int transparentPixelsN = 0;
    for (int i = 0; i < dataIntsN; i++) {
        if (pIntData[i] != ckey)
            continue;
        //here - have ckey pixel
        transparentPixelsN++;
        pIntData[i] = transparentValue;
    }
    if (transparentPixelsN == 0)
        return 0;
    //re-calculate transparent RGBs
    int dataCharsN = dataIntsN * 4;
    //duplicate image
    unsigned char* imgTemp = new unsigned char[dataCharsN];
    memcpy(imgTemp, imgData, dataCharsN);
    //scan
    int blurLevel = 1;
    int w00 = blurLevel * 2 + 1;
    for (int y0 = 0; y0 < h0; y0++) {
        int y1 = y0 - blurLevel;
        int h1 = w00;
        if (y1 < 0) {
            int d = -y1;
            y1 += d;
            h1 -= d;
        }
        else if (y1 > h0 - w00) {
            int d = y1 - (h0 - w00);
            h1 -= d;
        }
        for (int x0 = 0; x0 < w0; x0++) {
            if (imgData[(y0 * w0 + x0) * 4 + 3] != 0) //check current pixel's alpha component
                continue; //non-transparent pixel
            int x1 = x0 - blurLevel;
            int w1 = w00;
            if (x1 < 0) {
                int d = -x1;
                x1 += d;
                w1 -= d;
            }
            else if (x1 > w0 - w00) {
                int d = x1 - (w0 - w00);
                w1 -= d;
            }
            int sum[3] = { 0,0,0 };
            int opaquePixelsN = 0;
            for (int y = y1; y < y1 + h1; y++) {
                for (int x = x1; x < x1 + w1; x++) {
                    int idx = (y * w0 + x) * 4;
                    if (imgTemp[idx + 3] == 0) //check alpha channel
                        continue; //transparent pixel
                    opaquePixelsN++;
                    for (int ch = 0; ch < 3; ch++)
                        sum[ch] += imgTemp[idx + ch];
                }
            }
            if (opaquePixelsN == 0)
                continue;
            int idx = (y0 * w0 + x0) * 4;
            for (int ch = 0; ch < 3; ch++)
                imgData[idx + ch] = (unsigned char)(sum[ch] / opaquePixelsN);
        }
    }
    delete[] imgTemp;
    saveTGA("/dt/02.tga", imgData, w0, h0, 3);
    return 1;
}


В ModelLoader-е уже есть код, который читает и исполняет chroma key для текстуры. Надо просто добавить chroma key в model descriptor.

4. В Текстовом редакторе откроем

C:\CPP\a997modeler\dt\models\misc\marlboro01\root01.txt

и заменим код на:

<texture_as="tx0" src="marlboro03small.png" ckey="#00ff00"/>
<mt_type="phong" uTex0_use="tx0" />
<vs="box_tank" whl="53,83,21" ext=1 sectR=1 />
<a="front v,back v" xywh="2,1,323,495"/>
<a="right all" xywh="327,1,128,495"/>
<a="left all" xywh="457,1,128,495"/>
<a="top" xywh="588,1,323,133"/>
<a="bottom" xywh="587,136,324,134"/>

  • Заметим: добавлена новая переменная/property ckey=”#00ff00″.
  • Задана она в HTML HEX формате. Также ее можно задать как массив decimal integers: ckey=”0,255,0″.

5. Компиляция и запуск.

Теперь – без зеленых линий!


Проверено на Андроиде тоже – все норм.

Наша следующая цель – сделать изображение реалистичнее чем в реальности!


Leave a Reply

Your email address will not be published. Required fields are marked *