wraitii
326290463e
Please report any issue and/or crash. Those will get fixed in commits to come. Refs #48. This was SVN commit r15484.
469 lines
16 KiB
C++
469 lines
16 KiB
C++
/* Copyright (C) 2013 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* Water settings (speed, height) and texture management
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "graphics/Terrain.h"
|
|
#include "graphics/TextureManager.h"
|
|
|
|
#include "lib/bits.h"
|
|
#include "lib/timer.h"
|
|
#include "lib/tex/tex.h"
|
|
#include "lib/res/graphics/ogl_tex.h"
|
|
|
|
#include "maths/MathUtil.h"
|
|
#include "maths/Vector2D.h"
|
|
|
|
#include "ps/Game.h"
|
|
#include "ps/World.h"
|
|
|
|
#include "renderer/WaterManager.h"
|
|
#include "renderer/Renderer.h"
|
|
|
|
#include "simulation2/Simulation2.h"
|
|
#include "simulation2/components/ICmpWaterManager.h"
|
|
#include "simulation2/components/ICmpRangeManager.h"
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
// WaterManager implementation
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Construction/Destruction
|
|
WaterManager::WaterManager()
|
|
{
|
|
// water
|
|
m_RenderWater = false; // disabled until textures are successfully loaded
|
|
m_WaterHeight = 5.0f;
|
|
|
|
m_WaterCurrentTex = 0;
|
|
|
|
m_ReflectionTexture = 0;
|
|
m_RefractionTexture = 0;
|
|
m_ReflectionTextureSize = 0;
|
|
m_RefractionTextureSize = 0;
|
|
|
|
m_ReflectionFbo = 0;
|
|
m_RefractionFbo = 0;
|
|
|
|
m_WaterTexTimer = 0.0;
|
|
|
|
m_WindAngle = 0.0f;
|
|
m_Waviness = 8.0f;
|
|
m_WaterColor = CColor(0.3f, 0.35f, 0.7f, 1.0f);
|
|
m_WaterTint = CColor(0.28f, 0.3f, 0.59f, 1.0f);
|
|
m_Murkiness = 0.45f;
|
|
m_RepeatPeriod = 16.0f;
|
|
|
|
m_DistanceHeightmap = NULL;
|
|
m_BlurredNormalMap = NULL;
|
|
m_WindStrength = NULL;
|
|
|
|
m_WaterUgly = false;
|
|
m_WaterFancyEffects = false;
|
|
m_WaterRealDepth = false;
|
|
m_WaterRefraction = false;
|
|
m_WaterReflection = false;
|
|
m_WaterShadows = false;
|
|
m_WaterType = L"ocean";
|
|
|
|
m_NeedsReloading = false;
|
|
m_NeedInfoUpdate = true;
|
|
|
|
m_depthTT = 0;
|
|
m_FancyTexture = 0;
|
|
m_FboDepthTexture = 0;
|
|
|
|
m_MapSize = 0;
|
|
|
|
m_updatei0 = 0;
|
|
m_updatej0 = 0;
|
|
m_updatei1 = 0;
|
|
m_updatej1 = 0;
|
|
}
|
|
|
|
WaterManager::~WaterManager()
|
|
{
|
|
// Cleanup if the caller messed up
|
|
UnloadWaterTextures();
|
|
|
|
SAFE_ARRAY_DELETE(m_DistanceHeightmap);
|
|
SAFE_ARRAY_DELETE(m_BlurredNormalMap);
|
|
SAFE_ARRAY_DELETE(m_WindStrength);
|
|
|
|
glDeleteTextures(1, &m_depthTT);
|
|
glDeleteTextures(1, &m_FancyTexture);
|
|
glDeleteTextures(1, &m_FboDepthTexture);
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Progressive load of water textures
|
|
int WaterManager::LoadWaterTextures()
|
|
{
|
|
// TODO: this doesn't need to be progressive-loading any more
|
|
// (since texture loading is async now)
|
|
|
|
wchar_t pathname[PATH_MAX];
|
|
|
|
// Load diffuse grayscale images (for non-fancy water)
|
|
for (size_t i = 0; i < ARRAY_SIZE(m_WaterTexture); ++i)
|
|
{
|
|
swprintf_s(pathname, ARRAY_SIZE(pathname), L"art/textures/animated/water/default/diffuse%02d.dds", (int)i+1);
|
|
CTextureProperties textureProps(pathname);
|
|
textureProps.SetWrap(GL_REPEAT);
|
|
|
|
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
|
|
texture->Prefetch();
|
|
m_WaterTexture[i] = texture;
|
|
}
|
|
|
|
// Load normalmaps (for fancy water)
|
|
for (size_t i = 0; i < ARRAY_SIZE(m_NormalMap); ++i)
|
|
{
|
|
swprintf_s(pathname, ARRAY_SIZE(pathname), L"art/textures/animated/water/%ls/normal00%02d.png", m_WaterType.c_str(), (int)i+1);
|
|
CTextureProperties textureProps(pathname);
|
|
textureProps.SetWrap(GL_REPEAT);
|
|
textureProps.SetMaxAnisotropy(4);
|
|
|
|
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
|
|
texture->Prefetch();
|
|
m_NormalMap[i] = texture;
|
|
}
|
|
|
|
m_ReflectionTextureSize = g_Renderer.GetHeight() * 0.66; // Higher settings give a better result
|
|
m_RefractionTextureSize = g_Renderer.GetHeight() * 0.33; // Lower settings actually sorta look better since it blurs.
|
|
|
|
// Create reflection texture
|
|
glGenTextures(1, &m_ReflectionTexture);
|
|
glBindTexture(GL_TEXTURE_2D, m_ReflectionTexture);
|
|
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA,
|
|
(GLsizei)m_ReflectionTextureSize, (GLsizei)m_ReflectionTextureSize,
|
|
0, GL_RGB, GL_UNSIGNED_BYTE, 0);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
|
|
|
|
// Create refraction texture
|
|
glGenTextures(1, &m_RefractionTexture);
|
|
glBindTexture(GL_TEXTURE_2D, m_RefractionTexture);
|
|
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB,
|
|
(GLsizei)m_RefractionTextureSize, (GLsizei)m_RefractionTextureSize,
|
|
0, GL_RGB, GL_UNSIGNED_BYTE, 0);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
|
|
|
|
// Create depth texture
|
|
glGenTextures(1, &m_FboDepthTexture);
|
|
glBindTexture(GL_TEXTURE_2D, m_FboDepthTexture);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_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_DEPTH_COMPONENT, (GLsizei)m_ReflectionTextureSize, (GLsizei)m_ReflectionTextureSize, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL);
|
|
|
|
// Create the water framebuffers
|
|
m_ReflectionFbo = 0;
|
|
pglGenFramebuffersEXT(1, &m_ReflectionFbo);
|
|
pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_ReflectionFbo);
|
|
pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_ReflectionTexture, 0);
|
|
pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, m_FboDepthTexture, 0);
|
|
|
|
m_RefractionFbo = 0;
|
|
pglGenFramebuffersEXT(1, &m_RefractionFbo);
|
|
pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_RefractionFbo);
|
|
pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_RefractionTexture, 0);
|
|
pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, m_FboDepthTexture, 0);
|
|
|
|
pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
|
|
|
|
// Enable rendering, now that we've succeeded this far
|
|
m_RenderWater = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Unload water textures
|
|
void WaterManager::UnloadWaterTextures()
|
|
{
|
|
for(size_t i = 0; i < ARRAY_SIZE(m_WaterTexture); i++)
|
|
{
|
|
m_WaterTexture[i].reset();
|
|
}
|
|
|
|
for(size_t i = 0; i < ARRAY_SIZE(m_NormalMap); i++)
|
|
{
|
|
m_NormalMap[i].reset();
|
|
}
|
|
glDeleteTextures(1, &m_ReflectionTexture);
|
|
glDeleteTextures(1, &m_RefractionTexture);
|
|
pglDeleteFramebuffersEXT(1, &m_RefractionFbo);
|
|
pglDeleteFramebuffersEXT(1, &m_ReflectionFbo);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Calculate our binary heightmap from the terrain heightmap.
|
|
void WaterManager::RecomputeDistanceHeightmap()
|
|
{
|
|
if (m_DistanceHeightmap == NULL)
|
|
m_DistanceHeightmap = new u8[m_MapSize*m_MapSize];
|
|
|
|
// Custom copy the heightmap.
|
|
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
|
|
|
|
u16 waterLevel = m_WaterHeight/HEIGHT_SCALE;
|
|
|
|
u16* heightmap = terrain->GetHeightMap();
|
|
|
|
// We will "expand" the heightmap. That is we'll set each vertex on land as "3", and "bleed" that onto neighboring pixels.
|
|
// So 3 is "on land", 2 is "close", 1 "somewhat close" and 0 is "water".
|
|
// This gives a basic manhattan approximation of how close to the coast we are.
|
|
// I have a heathen fondness for ternary operators so there are some below.
|
|
u8 level = 0;
|
|
for (size_t z = 0; z < m_MapSize; ++z)
|
|
{
|
|
level = 0;
|
|
for (size_t x = 0; x < m_MapSize; ++x)
|
|
m_DistanceHeightmap[z*m_MapSize + x] = heightmap[z*m_MapSize + x] >= waterLevel ? level = 3
|
|
: level > 0 ? --level : 0;
|
|
level = 0;
|
|
for (size_t x = m_MapSize-1; x != (size_t)-1; --x)
|
|
{
|
|
if (heightmap[z*m_MapSize + x] >= waterLevel)
|
|
level = 3; // no need to set m_distanceHeightmap, it's already been done by the other loop.
|
|
else
|
|
{
|
|
level > 0 ? --level : 0;
|
|
if (level > m_DistanceHeightmap[z*m_MapSize + x])
|
|
m_DistanceHeightmap[z*m_MapSize + x] = level;
|
|
}
|
|
}
|
|
}
|
|
for (size_t x = 0; x < m_MapSize; ++x)
|
|
{
|
|
level = 0;
|
|
for (size_t z = 0; z < m_MapSize; ++z)
|
|
{
|
|
if (heightmap[z*m_MapSize + x] >= waterLevel)
|
|
level = 3;
|
|
else
|
|
{
|
|
level > 0 ? --level : 0;
|
|
if (level > m_DistanceHeightmap[z*m_MapSize + x])
|
|
m_DistanceHeightmap[z*m_MapSize + x] = level;
|
|
}
|
|
}
|
|
level = 0;
|
|
for (size_t z = m_MapSize-1; z != (size_t)-1; --z)
|
|
{
|
|
if (heightmap[z*m_MapSize + x] >= waterLevel)
|
|
level = 3;
|
|
else
|
|
{
|
|
level > 0 ? --level : 0;
|
|
if (level > m_DistanceHeightmap[z*m_MapSize + x])
|
|
m_DistanceHeightmap[z*m_MapSize + x] = level;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Calculate The blurred normal map to get an idea of where water ought to go.
|
|
void WaterManager::RecomputeBlurredNormalMap()
|
|
{
|
|
// used to cache terrain normals since otherwise we'd recalculate them a lot (I'm blurring the "normal" map).
|
|
// this might be updated to actually cache in the terrain manager but that's not for now.
|
|
if (m_BlurredNormalMap == NULL)
|
|
m_BlurredNormalMap = new CVector3D[m_MapSize*m_MapSize];
|
|
|
|
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
|
|
|
|
// It's really slow to calculate normals so cache them first.
|
|
CVector3D* normals = new CVector3D[m_MapSize*m_MapSize];
|
|
|
|
// Not the edges, we won't care about them.
|
|
float ii = 8.0f, jj = 8.0f;
|
|
for (size_t j = 2; j < m_MapSize-2; ++j, jj += 4.0f)
|
|
for (size_t i = 2; i < m_MapSize-2; ++i, ii += 4.0f)
|
|
{
|
|
CVector3D norm;
|
|
terrain->CalcNormal(i,j,norm);
|
|
normals[j*m_MapSize + i] = norm;
|
|
}
|
|
|
|
// We could be way fancier (and faster) for our blur but we probably don't need the complexity.
|
|
// Two pass filter, nothing complicated here.
|
|
CVector3D blurValue;
|
|
ii = 8.0f; jj = 8.0f;
|
|
size_t idx = 2;
|
|
for (size_t j = 2; j < m_MapSize-2; ++j, jj += 4.0f)
|
|
for (size_t i = 2; i < m_MapSize-2; ++i, ii += 4.0f,++idx)
|
|
{
|
|
blurValue = normals[idx-2];
|
|
blurValue += normals[idx-1];
|
|
blurValue += normals[idx];
|
|
blurValue += normals[idx+1];
|
|
blurValue += normals[idx+2];
|
|
m_BlurredNormalMap[idx] = blurValue * 0.2f;
|
|
}
|
|
// y direction, probably slower because of cache misses but I don't see an easy way around that.
|
|
ii = 8.0f; jj = 8.0f;
|
|
for (size_t i = 2; i < m_MapSize-2; ++i, ii += 4.0f)
|
|
{
|
|
for (size_t j = 2; j < m_MapSize-2; ++j, jj += 4.0f)
|
|
{
|
|
blurValue = normals[(j-2)*m_MapSize + i];
|
|
blurValue += normals[(j-1)*m_MapSize + i];
|
|
blurValue += normals[j*m_MapSize + i];
|
|
blurValue += normals[(j+1)*m_MapSize + i];
|
|
blurValue += normals[(j+2)*m_MapSize + i];
|
|
m_BlurredNormalMap[j*m_MapSize + i] = blurValue * 0.2f;
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Calculate the strength of the wind at a given point on the map.
|
|
// This is too slow and should support limited recomputation.
|
|
void WaterManager::RecomputeWindStrength()
|
|
{
|
|
if (m_WindStrength == NULL)
|
|
m_WindStrength = new float[m_MapSize*m_MapSize];
|
|
|
|
std::fill(m_WindStrength, m_WindStrength + m_MapSize*m_MapSize, 1.0f);
|
|
|
|
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
|
|
float waterLevel = m_WaterHeight;
|
|
|
|
CVector2D windDir = CVector2D(cos(m_WindAngle),sin(m_WindAngle));
|
|
// Our kernel will sample 5 points going towards the wind (generally).
|
|
int kernel[5][2] = { {windDir.X*2,windDir.Y*2}, {windDir.X*5,windDir.Y*5}, {windDir.X*9,windDir.Y*9}, {windDir.X*16,windDir.Y*16}, {windDir.X*25,windDir.Y*25} };
|
|
|
|
//CVector2D perp = CVector2D(-windDir.Y, windDir.X);
|
|
for (size_t j = 0; j < m_MapSize; ++j)
|
|
for (size_t i = 0; i < m_MapSize; ++i)
|
|
{
|
|
float curHeight = terrain->GetVertexGroundLevel(i,j);
|
|
if (curHeight >= waterLevel)
|
|
{
|
|
m_WindStrength[j*m_MapSize + i] = 0.0f;
|
|
continue;
|
|
}
|
|
if (terrain->GetVertexGroundLevel(i + ceil(windDir.X),j + ceil(windDir.Y)) < waterLevel)
|
|
continue;
|
|
|
|
// Calculate how dampened our waves should be.
|
|
float tendency = 0.0f;
|
|
float oldHeight = std::max(waterLevel,terrain->GetVertexGroundLevel(i+kernel[4][0],j+kernel[4][1]));
|
|
float currentHeight = std::max(waterLevel,terrain->GetVertexGroundLevel(i+kernel[3][0],j+kernel[3][1]));
|
|
tendency = currentHeight - oldHeight;
|
|
oldHeight = currentHeight;
|
|
currentHeight = std::max(waterLevel,terrain->GetVertexGroundLevel(i+kernel[2][0],j+kernel[2][1]));
|
|
tendency += currentHeight - oldHeight;
|
|
oldHeight = currentHeight;
|
|
currentHeight = std::max(waterLevel,terrain->GetVertexGroundLevel(i+kernel[1][0],j+kernel[1][1]));
|
|
tendency += currentHeight - oldHeight;
|
|
oldHeight = currentHeight;
|
|
currentHeight = std::max(waterLevel,terrain->GetVertexGroundLevel(i+kernel[0][0],j+kernel[0][1]));
|
|
tendency += currentHeight - oldHeight;
|
|
|
|
float baseLevel = std::max(0.0f,1.0f - (currentHeight-waterLevel)/20.0f);
|
|
baseLevel *= baseLevel;
|
|
tendency /= 10.0f;
|
|
baseLevel -= tendency; // if the terrain was sloping downwards, increase baselevel. Otherwise reduce.
|
|
baseLevel = clamp(baseLevel,0.0f,1.0f);
|
|
|
|
// Draw on map. This is pretty slow.
|
|
//float width = 3.0f;
|
|
float length = 21.0f * (1.0f-baseLevel);
|
|
|
|
for (float y = 0; y < length; y += 0.6f)
|
|
//for (float x = -width*(y+1)/(length+1); x < width*(y+1)/(length+1); x += 0.5f)
|
|
{
|
|
int index = (j - y * windDir.Y)*m_MapSize + (i - y * windDir.X);
|
|
m_WindStrength[index] = (0.5f+baseLevel*0.5f) * (1.0f-y/length) + y/length * 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// TODO: This will always recalculate for now
|
|
void WaterManager::SetMapSize(size_t size)
|
|
{
|
|
// TODO: Im' blindly trusting the user here.
|
|
m_MapSize = size;
|
|
m_NeedInfoUpdate = true;
|
|
m_updatei0 = 0;
|
|
m_updatei1 = size;
|
|
m_updatej0 = 0;
|
|
m_updatej1 = size;
|
|
|
|
SAFE_ARRAY_DELETE(m_DistanceHeightmap);
|
|
SAFE_ARRAY_DELETE(m_BlurredNormalMap);
|
|
SAFE_ARRAY_DELETE(m_WindStrength);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// This will set the bools properly
|
|
void WaterManager::UpdateQuality()
|
|
{
|
|
if (g_Renderer.GetOptionBool(CRenderer::OPT_WATERUGLY) != m_WaterUgly) {
|
|
m_WaterUgly = g_Renderer.GetOptionBool(CRenderer::OPT_WATERUGLY);
|
|
m_NeedsReloading = true;
|
|
}
|
|
if (g_Renderer.GetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS) != m_WaterFancyEffects) {
|
|
m_WaterFancyEffects = g_Renderer.GetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS);
|
|
m_NeedsReloading = true;
|
|
}
|
|
if (g_Renderer.GetOptionBool(CRenderer::OPT_WATERREALDEPTH) != m_WaterRealDepth) {
|
|
m_WaterRealDepth = g_Renderer.GetOptionBool(CRenderer::OPT_WATERREALDEPTH);
|
|
m_NeedsReloading = true;
|
|
}
|
|
if (g_Renderer.GetOptionBool(CRenderer::OPT_WATERREFRACTION) != m_WaterRefraction) {
|
|
m_WaterRefraction = g_Renderer.GetOptionBool(CRenderer::OPT_WATERREFRACTION);
|
|
m_NeedsReloading = true;
|
|
}
|
|
if (g_Renderer.GetOptionBool(CRenderer::OPT_WATERREFLECTION) != m_WaterReflection) {
|
|
m_WaterReflection = g_Renderer.GetOptionBool(CRenderer::OPT_WATERREFLECTION);
|
|
m_NeedsReloading = true;
|
|
}
|
|
if (g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWSONWATER) != m_WaterShadows) {
|
|
m_WaterShadows = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWSONWATER);
|
|
m_NeedsReloading = true;
|
|
}
|
|
}
|
|
|
|
bool WaterManager::WillRenderFancyWater()
|
|
{
|
|
if (!g_Renderer.GetCapabilities().m_FragmentShader)
|
|
return false;
|
|
if (!m_RenderWater || m_WaterUgly)
|
|
return false;
|
|
return true;
|
|
}
|