mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2024-11-29 00:55:41 +00:00
Added support for multiple texture sheets (with up to one per Model 3 texture format) as a rather brute-force way to handle overlapping texture formats in the current 3D engine. This fixes some corrupt textures in Daytona 2 and Virtua Striker 2 (and possibly other games) and also offers a small speed increase when some scenes load multiple overlapping textures.
This feature only enables itself when a compatible shader script is loaded. Since none have been checked in yet this means it is currently disabled.
This commit is contained in:
parent
35a47bc7e3
commit
84eb017744
|
@ -68,7 +68,7 @@
|
|||
#define VBO_VERTEX_OFFSET_TEXPARAMS_TRANS 20 // texture parameter: >=0 use transparency bit, <0 no transparency (per-polygon)
|
||||
#define VBO_VERTEX_OFFSET_TEXPARAMS_UWRAP 21 // texture parameters: U wrap mode: ==1 mirrored repeat, ==0 normal repeat
|
||||
#define VBO_VERTEX_OFFSET_TEXPARAMS_VWRAP 22 // "" V wrap mode ""
|
||||
#define VBO_VERTEX_OFFSET_TEXFORMAT_CONTOUR 23 // contour texture: >0 indicates contour texture (see also texParams.trans)
|
||||
#define VBO_VERTEX_OFFSET_TEXFORMAT 23 // texture format 0-7 (also ==0 indicates contour texture - see also texParams.trans)
|
||||
#define VBO_VERTEX_SIZE 24 // total size (may include padding for alignment)
|
||||
|
||||
|
||||
|
@ -154,7 +154,7 @@ void CRender3D::DrawDisplayList(ModelCache *Cache, POLY_STATE state)
|
|||
glColorPointer(3, GL_FLOAT, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_R*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(subTextureLoc, 4, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_TEXTURE_X*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(texParamsLoc, 4, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_TEXPARAMS_EN*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(texFormatLoc, 1, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_TEXFORMAT_CONTOUR*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(texFormatLoc, 1, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_TEXFORMAT*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(transLevelLoc, 1, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_TRANSLUCENCE*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(lightEnableLoc, 1, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_LIGHTENABLE*sizeof(GLfloat)));
|
||||
glVertexAttribPointer(shininessLoc, 1, GL_FLOAT, GL_FALSE, VBO_VERTEX_SIZE*sizeof(GLfloat), (GLvoid *) (VBO_VERTEX_OFFSET_SHININESS*sizeof(GLfloat)));
|
||||
|
@ -485,7 +485,7 @@ void CRender3D::InsertVertex(ModelCache *Cache, const Vertex *V, const Poly *P,
|
|||
Cache->verts[s][baseIdx + VBO_VERTEX_OFFSET_TEXPARAMS_TRANS] = contourProcessing;
|
||||
Cache->verts[s][baseIdx + VBO_VERTEX_OFFSET_TEXPARAMS_UWRAP] = (P->header[2]&2) ? 1.0f : 0.0f;
|
||||
Cache->verts[s][baseIdx + VBO_VERTEX_OFFSET_TEXPARAMS_VWRAP] = (P->header[2]&1) ? 1.0f : 0.0f;
|
||||
Cache->verts[s][baseIdx + VBO_VERTEX_OFFSET_TEXFORMAT_CONTOUR] = (texFormat==0) ? 1.0f : 0.0f;
|
||||
Cache->verts[s][baseIdx + VBO_VERTEX_OFFSET_TEXFORMAT] = (float)texFormat;
|
||||
|
||||
Cache->curVertIdx[s]++;
|
||||
Cache->vboCurOffset += VBO_VERTEX_SIZE*sizeof(GLfloat);
|
||||
|
|
|
@ -550,7 +550,6 @@ void CRender2D::MixLine(UINT32 *dest, const UINT32 *src, int layerNum, int y, bo
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void CRender2D::DrawTilemaps(UINT32 *destBottom, UINT32 *destTop)
|
||||
{
|
||||
// Base address of all 4 name tables
|
||||
|
@ -645,6 +644,7 @@ void CRender2D::DisplaySurface(int surface, GLfloat z)
|
|||
glLoadIdentity();
|
||||
|
||||
// Draw the surface
|
||||
glActiveTexture(GL_TEXTURE0); // texture unit 0
|
||||
glBindTexture(GL_TEXTURE_2D, texID[surface]);
|
||||
glBegin(GL_QUADS);
|
||||
glTexCoord2f(0.0f/512.0f, 0.0f); glVertex3f(0.0f, 0.0f, z);
|
||||
|
@ -697,6 +697,7 @@ void CRender2D::BeginFrame(void)
|
|||
|
||||
// Update all layers
|
||||
DrawTilemaps(surfBottom, surfTop);
|
||||
glActiveTexture(GL_TEXTURE0); // texture unit 0
|
||||
glBindTexture(GL_TEXTURE_2D, texID[0]);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 496, 384, GL_RGBA, GL_UNSIGNED_BYTE, surfTop);
|
||||
glBindTexture(GL_TEXTURE_2D, texID[1]);
|
||||
|
@ -801,6 +802,7 @@ bool CRender2D::Init(unsigned xOffset, unsigned yOffset, unsigned xRes, unsigned
|
|||
glGenTextures(2, texID);
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0); // texture unit 0
|
||||
glBindTexture(GL_TEXTURE_2D, texID[i]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
|
|
|
@ -202,10 +202,16 @@ void CRender3D::DecodeTexture(int format, int x, int y, int width, int height)
|
|||
return;
|
||||
}
|
||||
|
||||
// Check to see if ALL texture tiles have been properly decoded
|
||||
if ((textureFormat[y/32][x/32]==format) && (textureWidth[y/32][x/32]>=width) && (textureHeight[y/32][x/32]>=height))
|
||||
// Map Model3 format to texture unit and texture unit to texture sheet number
|
||||
unsigned texUnit = fmtToTexUnit[format];
|
||||
unsigned texNum = texUnit % numTexIDs; // there may be less texture sheets than texture units (due to lack of video memory)
|
||||
|
||||
// Check to see if ALL texture tiles have been properly decoded on current texture sheet
|
||||
if ((textureFormat[texNum][y/32][x/32]==format) && (textureWidth[texNum][y/32][x/32]>=width) && (textureHeight[texNum][y/32][x/32]>=height))
|
||||
return;
|
||||
|
||||
//printf("Decoding texture format %u: %u x %u @ (%u, %u) sheet %u\n", format, width, height, x, y, texNum);
|
||||
|
||||
// Copy and decode
|
||||
i = 0;
|
||||
switch (format)
|
||||
|
@ -382,19 +388,20 @@ void CRender3D::DecodeTexture(int format, int x, int y, int width, int height)
|
|||
|
||||
// Upload the texture
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glBindTexture(GL_TEXTURE_2D, texID);
|
||||
glActiveTexture(GL_TEXTURE0 + texUnit); // activate correct texture unit
|
||||
glBindTexture(GL_TEXTURE_2D, texIDs[texNum]); // bind correct texture sheet
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RGBA, GL_FLOAT, textureBuffer);
|
||||
|
||||
// Mark as decoded
|
||||
textureFormat[y/32][x/32] = format;
|
||||
textureWidth[y/32][x/32] = width;
|
||||
textureHeight[y/32][x/32] = height;
|
||||
textureFormat[texNum][y/32][x/32] = format;
|
||||
textureWidth[texNum][y/32][x/32] = width;
|
||||
textureHeight[texNum][y/32][x/32] = height;
|
||||
}
|
||||
|
||||
// Signals that new textures have been uploaded. Flushes model caches. Be careful not to exceed bounds!
|
||||
void CRender3D::UploadTextures(unsigned x, unsigned y, unsigned width, unsigned height)
|
||||
{
|
||||
unsigned xi, yi;
|
||||
unsigned texNum, xi, yi;
|
||||
|
||||
// Make everything red
|
||||
#ifdef DEBUG
|
||||
|
@ -407,12 +414,18 @@ void CRender3D::UploadTextures(unsigned x, unsigned y, unsigned width, unsigned
|
|||
}
|
||||
#endif
|
||||
|
||||
// Update all texture sheets
|
||||
for (texNum = 0; texNum < numTexIDs; texNum++)
|
||||
{
|
||||
for (xi = x/32; xi < (x+width)/32; xi++)
|
||||
{
|
||||
for (yi = y/32; yi < (y+height)/32; yi++)
|
||||
{
|
||||
textureFormat[yi][xi] = -1;
|
||||
textureWidth[yi][xi] = -1;
|
||||
textureHeight[yi][xi] = -1;
|
||||
textureFormat[texNum][yi][xi] = -1;
|
||||
textureWidth[texNum][yi][xi] = -1;
|
||||
textureHeight[texNum][yi][xi] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClearModelCache(&VROMCache);
|
||||
|
@ -962,11 +975,18 @@ void CRender3D::RenderFrame(void)
|
|||
glDepthFunc(GL_LESS);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
// Bind Real3D shader program and texture map
|
||||
// Bind Real3D shader program and texture maps
|
||||
glUseProgram(shaderProgram);
|
||||
glBindTexture(GL_TEXTURE_2D, texID);
|
||||
for (unsigned fmt = 0; fmt < 8; fmt++)
|
||||
{
|
||||
// Map Model3 format to texture unit and texture unit to texture sheet number
|
||||
unsigned texUnit = fmtToTexUnit[fmt];
|
||||
unsigned texNum = texUnit % numTexIDs; // there may be less texture sheets than texture units (due to lack of video memory)
|
||||
glActiveTexture(GL_TEXTURE0 + texUnit); // activate correct texture unit
|
||||
glBindTexture(GL_TEXTURE_2D, texIDs[texNum]); // bind correct texture sheet
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // fragment shader performs its own interpolation
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
}
|
||||
|
||||
// Enable VBO client states
|
||||
glEnableClientState(GL_VERTEX_ARRAY);
|
||||
|
@ -1066,19 +1086,7 @@ bool CRender3D::Init(unsigned xOffset, unsigned yOffset, unsigned xRes, unsigned
|
|||
if (NULL == textureBuffer)
|
||||
return ErrorLog("Insufficient memory for texture decode buffer.");
|
||||
|
||||
// Create texture map
|
||||
glGetError(); // clear error flag
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glGenTextures(1, &texID);
|
||||
glActiveTexture(GL_TEXTURE0); // texture unit 0
|
||||
glBindTexture(GL_TEXTURE_2D, texID);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // fragment shader performs its own interpolation
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2048, 2048, 0, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, 0);
|
||||
if (glGetError() != GL_NO_ERROR)
|
||||
return ErrorLog("OpenGL was unable to provide a 2048x2048-texel texture map.");
|
||||
|
||||
// Create model caches and VBOs
|
||||
if (CreateModelCache(&VROMCache, NUM_STATIC_VERTS, NUM_LOCAL_VERTS, NUM_STATIC_MODELS, 0x4000000/4, NUM_DISPLAY_LIST_ITEMS, false))
|
||||
|
@ -1108,10 +1116,76 @@ bool CRender3D::Init(unsigned xOffset, unsigned yOffset, unsigned xRes, unsigned
|
|||
if (OKAY != LoadShaderProgram(&shaderProgram,&vertexShader,&fragmentShader,vsFile,fsFile,vertexShaderSource,fragmentShaderSource))
|
||||
return FAIL;
|
||||
|
||||
// Bind the texture to the "textureMap" uniform so fragment shader can access it
|
||||
textureMapLoc = glGetUniformLocation(shaderProgram, "textureMap");
|
||||
// Query max number of texture units supported by video card to use as upper limit
|
||||
GLint glMaxTexUnits;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &glMaxTexUnits);
|
||||
unsigned maxTexUnits = max<int>(1, min<int>(g_Config.maxTexUnits, glMaxTexUnits));
|
||||
|
||||
// Try locating default "textureMap" uniform in shader program
|
||||
glUseProgram(shaderProgram); // bind program
|
||||
glUniform1i(textureMapLoc,0); // attach it to texture unit 0
|
||||
textureMapLoc = glGetUniformLocation(shaderProgram, "textureMap");
|
||||
unsigned unitCount = 0;
|
||||
if (textureMapLoc != -1)
|
||||
{
|
||||
// If exists, then bind to first texture unit
|
||||
unsigned texUnit = unitCount % maxTexUnits;
|
||||
glUniform1i(textureMapLoc, texUnit++);
|
||||
unitCount++;
|
||||
}
|
||||
|
||||
// Try locating "textureMap[0-7]" uniforms in shader program
|
||||
for (unsigned fmt = 0; fmt < 8; fmt++)
|
||||
{
|
||||
char uniformName[12];
|
||||
sprintf(uniformName, "textureMap%u", fmt);
|
||||
textureMapLocs[fmt] = glGetUniformLocation(shaderProgram, uniformName);
|
||||
if (textureMapLocs[fmt] != -1)
|
||||
{
|
||||
// If exists, then bind to next texture unit
|
||||
unsigned texUnit = unitCount % maxTexUnits;
|
||||
glUniform1i(textureMapLocs[fmt], texUnit);
|
||||
fmtToTexUnit[fmt] = texUnit;
|
||||
unitCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise bind to first texture unit by default
|
||||
fmtToTexUnit[fmt] = 0;
|
||||
}
|
||||
}
|
||||
numTexUnits = min<int>(unitCount, maxTexUnits);
|
||||
|
||||
// Check located at least one uniform to bind to a texture unit
|
||||
if (numTexUnits == 0)
|
||||
return ErrorLog("Could not locate any textureMap uniforms in shader script.");
|
||||
InfoLog("Located and bound %u uniform(s) in GL shader script to %u texture unit(s).", numTexUnits, unitCount);
|
||||
|
||||
// Now try creating texture sheets, one for each texture unit (memory permitting)
|
||||
numTexIDs = numTexUnits;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glGenTextures(numTexIDs, texIDs);
|
||||
for (unsigned texNum = 0; texNum < numTexIDs; texNum++)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + texNum); // activate correct texture unit
|
||||
glBindTexture(GL_TEXTURE_2D, texIDs[texNum]); // bind correct texture sheet
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // fragment shader performs its own interpolation
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2048, 2048, 0, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, 0);
|
||||
if (glGetError() != GL_NO_ERROR)
|
||||
{
|
||||
// Probably ran out of video memory, so don't try creating any more texture sheets
|
||||
numTexIDs = texNum;
|
||||
glGetError(); // clear error flag
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check created at least one texture sheet
|
||||
if (numTexIDs == 0)
|
||||
return ErrorLog("OpenGL was unable to provide any 2048x2048-texel texture maps.");
|
||||
InfoLog("Created %u 2048x2048-texel GL texture map(s)", numTexIDs);
|
||||
|
||||
// Get location of the rest of the uniforms
|
||||
modelViewMatrixLoc = glGetUniformLocation(shaderProgram,"modelViewMatrix");
|
||||
|
@ -1180,7 +1254,7 @@ CRender3D::~CRender3D(void)
|
|||
DestroyShaderProgram(shaderProgram,vertexShader,fragmentShader);
|
||||
if (glBindBuffer != NULL) // we may have failed earlier due to lack of OpenGL 2.0 functions
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0); // disable VBOs by binding to 0
|
||||
glDeleteTextures(1,&texID);
|
||||
glDeleteTextures(numTexIDs, texIDs);
|
||||
|
||||
DestroyModelCache(&VROMCache);
|
||||
DestroyModelCache(&PolyCache);
|
||||
|
|
|
@ -177,11 +177,13 @@ class CRender3DConfig
|
|||
public:
|
||||
string vertexShaderFile; // path to vertex shader or "" to use internal shader
|
||||
string fragmentShaderFile; // fragment shader
|
||||
unsigned maxTexUnits; // maximum number of texture units to use (1-9)
|
||||
|
||||
// Defaults
|
||||
CRender3DConfig(void)
|
||||
{
|
||||
// nothing to do, strings will be clear to begin with
|
||||
// strings will be clear to begin with
|
||||
maxTexUnits = 9;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -382,27 +384,31 @@ private:
|
|||
unsigned xOffs, yOffs;
|
||||
unsigned totalXRes, totalYRes;
|
||||
|
||||
// Texture ID for complete 2048x2048 texture map
|
||||
GLuint texID;
|
||||
// Texture details
|
||||
unsigned numTexUnits; // number of texture units
|
||||
int fmtToTexUnit[8]; // mapping from Model3 texture format to texture unit
|
||||
unsigned numTexIDs; // number of 2048x2048 texture sheets (maximum 8, one for each Model3 texture format)
|
||||
GLuint texIDs[8]; // texture IDs of texture sheets
|
||||
|
||||
// Shader programs and input data locations
|
||||
GLuint shaderProgram; // shader program object
|
||||
GLuint vertexShader; // vertex shader handle
|
||||
GLuint fragmentShader; // fragment shader
|
||||
GLuint textureMapLoc; // location of "textureMap" uniform
|
||||
GLuint modelViewMatrixLoc; // uniform
|
||||
GLuint projectionMatrixLoc; // uniform
|
||||
GLuint lightingLoc; // uniform
|
||||
GLuint spotEllipseLoc; // uniform
|
||||
GLuint spotRangeLoc; // uniform
|
||||
GLuint spotColorLoc; // uniform
|
||||
GLuint subTextureLoc; // attribute
|
||||
GLuint texParamsLoc; // attribute
|
||||
GLuint texFormatLoc; // attribute
|
||||
GLuint transLevelLoc; // attribute
|
||||
GLuint lightEnableLoc; // attribute
|
||||
GLuint shininessLoc; // attribute
|
||||
GLuint fogIntensityLoc; // attribute
|
||||
GLint textureMapLoc; // location of "textureMap" uniform (default combined sheet for all Model3 textures formats)
|
||||
GLint textureMapLocs[8]; // location of "textureMap[0-7]" uniforms (one sheet per Model3 texture format)
|
||||
GLint modelViewMatrixLoc; // uniform
|
||||
GLint projectionMatrixLoc; // uniform
|
||||
GLint lightingLoc; // uniform
|
||||
GLint spotEllipseLoc; // uniform
|
||||
GLint spotRangeLoc; // uniform
|
||||
GLint spotColorLoc; // uniform
|
||||
GLint subTextureLoc; // attribute
|
||||
GLint texParamsLoc; // attribute
|
||||
GLint texFormatLoc; // attribute
|
||||
GLint transLevelLoc; // attribute
|
||||
GLint lightEnableLoc; // attribute
|
||||
GLint shininessLoc; // attribute
|
||||
GLint fogIntensityLoc; // attribute
|
||||
|
||||
// Model caching
|
||||
ModelCache VROMCache; // VROM (static) models
|
||||
|
@ -417,9 +423,9 @@ private:
|
|||
* correspond to the texture format bits in the polygon headers. They can
|
||||
* be used to determine whether a texture needs to be updated.
|
||||
*/
|
||||
int textureWidth[2048/32][2048/32];
|
||||
int textureHeight[2048/32][2048/32];
|
||||
INT8 textureFormat[2048/32][2048/32];
|
||||
int textureWidth[8][2048/32][2048/32];
|
||||
int textureHeight[8][2048/32][2048/32];
|
||||
INT8 textureFormat[8][2048/32][2048/32];
|
||||
|
||||
/*
|
||||
* Texture Decode Buffer
|
||||
|
|
|
@ -167,7 +167,7 @@ void main(void)
|
|||
}
|
||||
|
||||
// If contour texture and not discarded, force alpha to 1.0 because will later be modified by polygon translucency
|
||||
if (fsTexFormat > 0.0) // contour (T1RGB5) texture map
|
||||
if (fsTexFormat < 0.5) // contour (T1RGB5) texture map
|
||||
fragColor.a = 1.0;
|
||||
}
|
||||
|
||||
|
|
|
@ -384,7 +384,7 @@ static const char fragmentShaderSource[] =
|
|||
"\t\t}\n"
|
||||
"\t\t\n"
|
||||
"\t\t// If contour texture and not discarded, force alpha to 1.0 because will later be modified by polygon translucency\n"
|
||||
"\t\tif (fsTexFormat > 0.0)\t\t// contour (T1RGB5) texture map\n"
|
||||
"\t\tif (fsTexFormat < 0.5)\t\t// contour (T1RGB5) texture map\n"
|
||||
"\t\t\tfragColor.a = 1.0;\n"
|
||||
"\t}\n"
|
||||
"\n"
|
||||
|
|
Loading…
Reference in a new issue