From fa12f7e35e26be390a8a64fc902a8c61bcae4063 Mon Sep 17 00:00:00 2001 From: Bastien Bouclet Date: Sun, 24 Feb 2013 13:32:00 +0100 Subject: [PATCH] MYST3: Implement the water effect The effect looks similar enough to the original but not exactly the same. This is CPU intensive. Once well tested it may be worth reimplementing it as a shader. --- engines/myst3/console.cpp | 30 +++- engines/myst3/console.h | 1 + engines/myst3/cursor.cpp | 2 +- engines/myst3/effects.cpp | 288 ++++++++++++++++++++++++++++++++++++++ engines/myst3/effects.h | 90 ++++++++++++ engines/myst3/module.mk | 1 + engines/myst3/node.cpp | 125 ++++++++++------- engines/myst3/node.h | 7 +- engines/myst3/puzzles.cpp | 4 +- engines/myst3/state.cpp | 2 +- engines/myst3/state.h | 2 +- 11 files changed, 489 insertions(+), 63 deletions(-) create mode 100644 engines/myst3/effects.cpp create mode 100644 engines/myst3/effects.h diff --git a/engines/myst3/console.cpp b/engines/myst3/console.cpp index 2bc59476d98..6a3f75e6dc8 100644 --- a/engines/myst3/console.cpp +++ b/engines/myst3/console.cpp @@ -22,6 +22,7 @@ #include "engines/myst3/console.h" #include "engines/myst3/database.h" +#include "engines/myst3/effects.h" #include "engines/myst3/inventory.h" #include "engines/myst3/script.h" #include "engines/myst3/state.h" @@ -340,15 +341,15 @@ bool Console::Cmd_DumpMasks(int argc, const char **argv) { DebugPrintf("Extracting masks for node %d:\n", nodeId); for (uint i = 0; i < 6; i++) { - bool water = _vm->_node->dumpFaceMask(nodeId, i, DirectorySubEntry::kWaterEffectMask); + bool water = dumpFaceMask(nodeId, i, DirectorySubEntry::kWaterEffectMask); if (water) DebugPrintf("Face %d: water OK\n", i); - bool effect2 = _vm->_node->dumpFaceMask(nodeId, i, DirectorySubEntry::kEffect2Mask); + bool effect2 = dumpFaceMask(nodeId, i, DirectorySubEntry::kEffect2Mask); if (effect2) DebugPrintf("Face %d: effect 2 OK\n", i); - bool magnet = _vm->_node->dumpFaceMask(nodeId, i, DirectorySubEntry::kMagneticEffectMask); + bool magnet = dumpFaceMask(nodeId, i, DirectorySubEntry::kMagneticEffectMask); if (magnet) DebugPrintf("Face %d: magnet OK\n", i); @@ -359,4 +360,27 @@ bool Console::Cmd_DumpMasks(int argc, const char **argv) { return true; } +bool Console::dumpFaceMask(uint16 index, int face, DirectorySubEntry::ResourceType type) { + const DirectorySubEntry *maskDesc = _vm->getFileDescription(0, index, face, type); + + if (!maskDesc) + return false; + + Common::MemoryReadStream *maskStream = maskDesc->getData(); + + Graphics::Surface *mask = Effect::loadMask(maskStream); + + delete maskStream; + + Common::DumpFile outFile; + outFile.open(Common::String::format("dump/%d-%d.masku_%d", index, face, type)); + outFile.write(mask->getPixels(), mask->pitch * mask->h); + outFile.close(); + + mask->free(); + delete mask; + + return true; +} + } /* namespace Myst3 */ diff --git a/engines/myst3/console.h b/engines/myst3/console.h index c8d9ab9b7af..27263b43a66 100644 --- a/engines/myst3/console.h +++ b/engines/myst3/console.h @@ -41,6 +41,7 @@ private: Myst3Engine *_vm; void describeScript(const Common::Array &script); + bool dumpFaceMask(uint16 index, int face, DirectorySubEntry::ResourceType type); bool Cmd_Infos(int argc, const char **argv); bool Cmd_LookAt(int argc, const char **argv); diff --git a/engines/myst3/cursor.cpp b/engines/myst3/cursor.cpp index ac41e6396fb..5a206409039 100644 --- a/engines/myst3/cursor.cpp +++ b/engines/myst3/cursor.cpp @@ -125,7 +125,7 @@ void Cursor::lockPosition(bool lock) { _lockedAtCenter = lock; - g_system->lockMouse(lock); + //g_system->lockMouse(lock); if (_lockedAtCenter) { // Locking, just mouve the cursor at the center of the screen diff --git a/engines/myst3/effects.cpp b/engines/myst3/effects.cpp new file mode 100644 index 00000000000..93468befa10 --- /dev/null +++ b/engines/myst3/effects.cpp @@ -0,0 +1,288 @@ +/* ResidualVM - A 3D game interpreter + * + * ResidualVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * file distributed with this source distribution. + * + * This program 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. + + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "engines/myst3/effects.h" +#include "engines/myst3/myst3.h" +#include "engines/myst3/state.h" + +namespace Myst3 { + +Effect::Effect(Myst3Engine *vm) : + _vm(vm) { +} + +Effect::~Effect() { + for (FaceMaskMap::iterator it = _facesMasks.begin(); it != _facesMasks.end(); it++) { + it->_value->free(); + delete it->_value; + } +} + +bool Effect::loadMasks(uint32 id, DirectorySubEntry::ResourceType type) { + // Just in case + _facesMasks.clear(); + + bool isFrame = _vm->_state->getViewType() == kFrame; + + // Load the mask of each face + for (uint i = 0; i < 6; i++) { + const DirectorySubEntry *desc = _vm->getFileDescription(0, id, i + 1, type); + + if (desc) { + Common::SeekableReadStream *data = desc->getData(); + + _facesMasks[i] = loadMask(data); + + // Frame masks are vertically flipped for some reason + if (isFrame) { + flipVertical(_facesMasks[i]); + } + + delete data; + } + } + + if (_facesMasks.empty()) + return false; + + return true; +} + +Graphics::Surface *Effect::loadMask(Common::SeekableReadStream *maskStream) { + Graphics::Surface *s = new Graphics::Surface(); + s->create(640, 640, Graphics::PixelFormat::createFormatCLUT8()); + + uint32 headerOffset = 0; + uint32 dataOffset = 0; + + while (headerOffset < 400) { + int blockX = (headerOffset / sizeof(dataOffset)) % 10; + int blockY = (headerOffset / sizeof(dataOffset)) / 10; + + maskStream->seek(headerOffset, SEEK_SET); + dataOffset = maskStream->readUint32LE(); + headerOffset = maskStream->pos(); + + if (dataOffset != 0) { + maskStream->seek(dataOffset, SEEK_SET); + + for(int i = 63; i >= 0; i--) { + int x = 0; + byte numValues = maskStream->readByte(); + for (int j = 0; j < numValues; j++) { + byte repeat = maskStream->readByte(); + byte value = maskStream->readByte(); + for (int k = 0; k < repeat; k++) { + ((uint8*)s->getPixels())[((blockY * 64) + i) * 640 + blockX * 64 + x] = value; + x++; + } + } + } + } + } + + return s; +} + +void Effect::flipVertical(Graphics::Surface *s) { + for (int y = 0; y < s->h / 2; ++y) { + // Flip the lines + byte *line1P = (byte *)s->getBasePtr(0, y); + byte *line2P = (byte *)s->getBasePtr(0, s->h - y - 1); + + for (int x = 0; x < s->w; ++x) + SWAP(line1P[x], line2P[x]); + } +} + +WaterEffect::WaterEffect(Myst3Engine *vm) : + Effect(vm), + _lastUpdate(0), + _step(0) { +} + +WaterEffect::~WaterEffect() { + +} + +WaterEffect *WaterEffect::create(Myst3Engine *vm, uint32 id) { + WaterEffect *s = new WaterEffect(vm); + + if (!s->loadMasks(id, DirectorySubEntry::kWaterEffectMask)) { + delete s; + return 0; + } + + return s; +} + +bool WaterEffect::isRunning() { + return _vm->_state->getWaterEffectActive() + && _vm->_state->getWaterEffectRunning(); +} + +bool WaterEffect::update() { + if (!isRunning()) { + return false; + } + + if (g_system->getMillis() - _lastUpdate >= 1000 / (uint32)_vm->_state->getWaterEffectSpeed()) { + _lastUpdate = g_system->getMillis(); + + _step++; + if (_step > _vm->_state->getWaterEffectMaxStep()) + _step = 0; + + float position = _step / (float)_vm->_state->getWaterEffectMaxStep(); + + doStep(position, _vm->_state->getViewType() == kFrame); + + return true; + } + + return false; +} + +void WaterEffect::doStep(float position, bool isFrame) { + double timeOffset; + double frequency; + double ampl; + + timeOffset = position * 2 * M_PI; + frequency = _vm->_state->getWaterEffectFrequency() * 0.1; + + ampl = _vm->_state->getWaterEffectAmpl() / 10.0 / 2.0; + for (uint i = 0; i < 640; i++) { + double ampl1; + if (i < 320) + ampl1 = i / 320 + 1.0; + else + ampl1 = (640 - i) / 320 + 1.0; + + _bottomDisplacement[i] = sin(i / 640.0 * frequency * 2 * M_PI + timeOffset) / 2 * ampl1 * ampl; + } + + // FIXME: The original sets this to WaterEffectAttenuation, which causes + // glitches here + uint32 attenuation = 640; + for (uint i = 0; i < attenuation; i++) { + double ampl2 = attenuation / (attenuation - i + 1.0); + + int8 value = sin(i / 640.0 * frequency * 2 * M_PI * ampl2 + timeOffset) / 2 * 1.0 / ampl2 * ampl; + + if (!isFrame) { + _verticalDisplacement[i] = value; + } else { + _verticalDisplacement[attenuation - 1 - i] = value; + } + } + + for (uint i = 0; i < 640; i++) { + double ampl3 = sin(i / 640.0 * frequency * 2 * M_PI + timeOffset) / 2.0; + + _horizontalDisplacements[0][i] = ampl3 * 1.25 * ampl + 0.5; + _horizontalDisplacements[1][i] = ampl3 * 1.00 * ampl + 0.5; + _horizontalDisplacements[2][i] = ampl3 * 0.75 * ampl + 0.5; + _horizontalDisplacements[3][i] = ampl3 * 0.50 * ampl + 0.5; + _horizontalDisplacements[4][i] = ampl3 * 0.25 * ampl + 0.5; + } +} + +void WaterEffect::applyForFace(uint face, Graphics::Surface *src, Graphics::Surface *dst) { + if (!isRunning()) { + return; + } + + Graphics::Surface *mask = _facesMasks.getVal(face); + + if (!mask) + error("No mask for face %d", face); + + apply(src, dst, mask, face == 1, _vm->_state->getWaterEffectAmpl()); +} + +void WaterEffect::apply(Graphics::Surface *src, Graphics::Surface *dst, Graphics::Surface *mask, bool bottomFace, int32 waterEffectAmpl) { + + int8 *hDisplacement; + int8 *vDisplacement; + + if (bottomFace) { + hDisplacement = _bottomDisplacement; + vDisplacement = _bottomDisplacement; + } else { + vDisplacement = _verticalDisplacement; + } + + + uint32 *dstPtr = (uint32 *)dst->getPixels(); + byte *maskPtr = (byte *)mask->getPixels(); + + for (uint y = 0; y < dst->h; y++) { + if (!bottomFace) { + uint32 strength = (320 * (9 - y / 64)) / _vm->_state->getWaterEffectAttenuation(); + if (strength > 4) + strength = 4; + hDisplacement = _horizontalDisplacements[strength]; + } + + for (uint x = 0; x < dst->w; x++) { + int8 maskValue = *maskPtr; + + if (maskValue != 0) { + int8 xOffset = hDisplacement[x]; + int8 yOffset = vDisplacement[y]; + + if (maskValue < 8) { + maskValue -= _vm->_state->getWaterEffectAmplOffset(); + if (maskValue < 0) { + maskValue = 0; + } + + if (xOffset >= 0) { + if (xOffset > maskValue) + xOffset = maskValue; + } else { + if (-xOffset > maskValue) + xOffset = -maskValue; + } + if (yOffset >= 0) { + if (yOffset > maskValue) + yOffset = maskValue; + } else { + if (-yOffset > maskValue) + yOffset = -maskValue; + } + } + + uint32 srcValue1 = *(uint32 *) src->getBasePtr(x + xOffset, y + yOffset); + uint32 srcValue2 = *(uint32 *) src->getBasePtr(x, y); + + *dstPtr = 0xFF000000 | ((0x007F7F7F & (srcValue1 >> 1)) + (0x007F7F7F & (srcValue2 >> 1))); + } + + maskPtr++; + dstPtr++; + } + } +} + +} /* namespace Myst3 */ diff --git a/engines/myst3/effects.h b/engines/myst3/effects.h new file mode 100644 index 00000000000..44bbf5f6d70 --- /dev/null +++ b/engines/myst3/effects.h @@ -0,0 +1,90 @@ +/* ResidualVM - A 3D game interpreter + * + * ResidualVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * file distributed with this source distribution. + * + * This program 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. + + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef EFFECTS_H_ +#define EFFECTS_H_ + +#include "common/hashmap.h" + +#include "engines/myst3/directorysubentry.h" + +namespace Graphics { +class Surface; +} + +namespace Myst3 { + +class Myst3Engine; + +class Effect { +public: + virtual ~Effect(); + + virtual bool update() = 0; + virtual void applyForFace(uint face, Graphics::Surface *src, Graphics::Surface *dst) = 0; + + bool hasFace(uint face) { return _facesMasks.contains(face); } + + // Public and static for use by the debug console + static Graphics::Surface *loadMask(Common::SeekableReadStream *maskStream); + +protected: + Effect(Myst3Engine *vm); + + bool loadMasks(uint32 id, DirectorySubEntry::ResourceType type); + void flipVertical(Graphics::Surface *s); + + Myst3Engine *_vm; + + typedef Common::HashMap FaceMaskMap; + FaceMaskMap _facesMasks; + +}; + +class WaterEffect : public Effect { +public: + static WaterEffect *create(Myst3Engine *vm, uint32 id); + virtual ~WaterEffect(); + + bool update(); + void applyForFace(uint face, Graphics::Surface *src, Graphics::Surface *dst); + +protected: + WaterEffect(Myst3Engine *vm); + + void doStep(float position, bool isFrame); + void apply(Graphics::Surface *src, Graphics::Surface *dst, Graphics::Surface *mask, + bool bottomFace, int32 waterEffectAmpl); + + uint32 _lastUpdate; + int32 _step; + + int8 _bottomDisplacement[640]; + int8 _verticalDisplacement[640]; + int8 _horizontalDisplacements[5][640]; + +private: + bool isRunning(); +}; + +} /* namespace Myst3 */ +#endif /* EFFECTS_H_ */ diff --git a/engines/myst3/module.mk b/engines/myst3/module.mk index 757807f5526..8fe75d286ce 100644 --- a/engines/myst3/module.mk +++ b/engines/myst3/module.mk @@ -9,6 +9,7 @@ MODULE_OBJS := \ detection.o \ directoryentry.o \ directorysubentry.o \ + effects.o \ gfx.o \ gfx_opengl.o \ gfx_opengl_shaders.o \ diff --git a/engines/myst3/node.cpp b/engines/myst3/node.cpp index 03f98430bcd..53d981fa8ad 100644 --- a/engines/myst3/node.cpp +++ b/engines/myst3/node.cpp @@ -20,6 +20,7 @@ * */ +#include "engines/myst3/effects.h" #include "engines/myst3/node.h" #include "engines/myst3/myst3.h" #include "engines/myst3/state.h" @@ -38,12 +39,18 @@ void Face::setTextureFromJPEG(const DirectorySubEntry *jpegDesc) { Face::Face(Myst3Engine *vm) : _vm(vm), _textureDirty(true), - _texture(0) { + _texture(0), + _bitmap(0), + _finalBitmap(0) { } void Face::uploadTexture() { if (_textureDirty) { - _texture->update(_bitmap); + if (_finalBitmap) + _texture->update(_finalBitmap); + else + _texture->update(_bitmap); + _textureDirty = false; } } @@ -53,6 +60,11 @@ Face::~Face() { delete _bitmap; _bitmap = 0; + if (_finalBitmap) { + _finalBitmap->free(); + delete _finalBitmap; + } + if (_texture) { _vm->_gfx->freeTexture(_texture); } @@ -63,60 +75,14 @@ Node::Node(Myst3Engine *vm, uint16 id) : _subtitles(0) { for (uint i = 0; i < 6; i++) _faces[i] = 0; -} -bool Node::dumpFaceMask(uint16 index, int face, DirectorySubEntry::ResourceType type) { - static const int32 kMaskSize = 640 * 640; - - byte *mask = new byte[kMaskSize]; - memset(mask, 0, kMaskSize); - uint32 headerOffset = 0; - uint32 dataOffset = 0; - - const DirectorySubEntry *maskDesc = _vm->getFileDescription(0, index, face, type); - - if (!maskDesc) { - delete[] mask; - return false; - } - - Common::MemoryReadStream *maskStream = maskDesc->getData(); - - while (headerOffset < 400) { - int blockX = (headerOffset / sizeof(dataOffset)) % 10; - int blockY = (headerOffset / sizeof(dataOffset)) / 10; - - maskStream->seek(headerOffset, SEEK_SET); - dataOffset = maskStream->readUint32LE(); - headerOffset = maskStream->pos(); - - if (dataOffset != 0) { - maskStream->seek(dataOffset, SEEK_SET); - - for(int i = 63; i >= 0; i--) { - int x = 0; - byte numValues = maskStream->readByte(); - for (int j = 0; j < numValues; j++) { - byte repeat = maskStream->readByte(); - byte value = maskStream->readByte(); - for (int k = 0; k < repeat; k++) { - mask[((blockY * 64) + i) * 640 + blockX * 64 + x] = value; - x++; - } - } - } + if (_vm->_state->getWaterEffects()) { + Effect *effect = WaterEffect::create(vm, id); + if (effect) { + _effects.push_back(effect); + _vm->_state->setWaterEffectActive(true); } } - - delete maskStream; - - Common::DumpFile outFile; - outFile.open(Common::String::format("dump/%d-%d.masku_%d", index, face, type)); - outFile.write(mask, kMaskSize); - outFile.close(); - delete[] mask; - - return true; } Node::~Node() { @@ -125,6 +91,12 @@ Node::~Node() { } _spotItems.clear(); + for (uint i = 0; i < _effects.size(); i++) { + delete _effects[i]; + } + _effects.clear(); + _vm->_state->setWaterEffectActive(false); + for (int i = 0; i < 6; i++) { delete _faces[i]; } @@ -206,6 +178,53 @@ void Node::update() { for (uint i = 0; i < _spotItems.size(); i++) { _spotItems[i]->updateDraw(); } + + bool needsUpdate = false; + for (uint i = 0; i < _effects.size(); i++) { + needsUpdate |= _effects[i]->update(); + } + + // Apply the effects for all the faces + for (uint faceId = 0; faceId < 6; faceId++) { + Face *face = _faces[faceId]; + + if (face == 0) continue; + + uint effectsForFace = 0; + for (uint i = 0; i < _effects.size(); i++) { + if (_effects[i]->hasFace(faceId)) + effectsForFace++; + } + + if (effectsForFace == 0) continue; + if (!needsUpdate && !face->isTextureDirty()) continue; + + // Alloc the target surface if necessary + if (!face->_finalBitmap) { + face->_finalBitmap = new Graphics::Surface(); + face->_finalBitmap->copyFrom(*face->_bitmap); + } + + if (effectsForFace == 1) { + _effects[0]->applyForFace(faceId, face->_bitmap, face->_finalBitmap); + + face->markTextureDirty(); + } else if (effectsForFace == 2) { + // TODO: Keep the same temp surface to avoid heap fragmentation ? + Graphics::Surface *tmp = new Graphics::Surface(); + tmp->copyFrom(*face->_bitmap); + + _effects[0]->applyForFace(faceId, face->_bitmap, tmp); + _effects[1]->applyForFace(faceId, tmp, face->_finalBitmap); + + tmp->free(); + delete tmp; + + face->markTextureDirty(); + } else { + error("Unable to render more than 2 effects per faceId (%d)", effectsForFace); + } + } } SpotItem::SpotItem(Myst3Engine *vm) : diff --git a/engines/myst3/node.h b/engines/myst3/node.h index d60ca704bb8..3af796b1b13 100644 --- a/engines/myst3/node.h +++ b/engines/myst3/node.h @@ -36,10 +36,12 @@ namespace Myst3 { class Texture; class Myst3Engine; class Subtitles; +class Effect; class Face { public: Graphics::Surface *_bitmap; + Graphics::Surface *_finalBitmap; Texture *_texture; Face(Myst3Engine *vm); @@ -48,6 +50,8 @@ class Face { void setTextureFromJPEG(const DirectorySubEntry *jpegDesc); void markTextureDirty() { _textureDirty = true; } + bool isTextureDirty() { return _textureDirty; } + void uploadTexture(); private: @@ -126,6 +130,7 @@ class Node : Drawable { Face *_faces[6]; Common::Array _spotItems; Subtitles *_subtitles; + Common::Array _effects; public: Node(Myst3Engine *vm, uint16 id); @@ -140,8 +145,6 @@ class Node : Drawable { void loadSubtitles(uint32 id); bool hasSubtitlesToDraw(); - - bool dumpFaceMask(uint16 index, int face, DirectorySubEntry::ResourceType type); }; } // end of namespace Myst3 diff --git a/engines/myst3/puzzles.cpp b/engines/myst3/puzzles.cpp index a14b752c985..c2d4b5c4d30 100644 --- a/engines/myst3/puzzles.cpp +++ b/engines/myst3/puzzles.cpp @@ -634,7 +634,7 @@ void Puzzles::pinball(int16 var) { if (var >= 0) return; - _vm->_state->setWaterEffectPaused(false); + _vm->_state->setWaterEffectRunning(false); // Remove the default panel movies _vm->removeMovie(10116); @@ -840,7 +840,7 @@ void Puzzles::pinball(int16 var) { _vm->_sound->playEffect(1028, 50); } else if (jumpType == 1 || jumpType == 4) { _vm->_state->setVar(26, jumpType); - _vm->_state->setWaterEffectPaused(true); + _vm->_state->setWaterEffectRunning(true); // sound fade stop 1025, 7 return; } diff --git a/engines/myst3/state.cpp b/engines/myst3/state.cpp index 97b52c00847..4e4700dc7e8 100644 --- a/engines/myst3/state.cpp +++ b/engines/myst3/state.cpp @@ -112,7 +112,7 @@ GameState::GameState(Myst3Engine *vm): VAR(92, HotspotActiveRect, false) - VAR(93, WaterEffectPaused, true) + VAR(93, WaterEffectRunning, true) VAR(94, WaterEffectActive, true) VAR(95, WaterEffectSpeed, true) VAR(96, WaterEffectAttenuation, true) diff --git a/engines/myst3/state.h b/engines/myst3/state.h index 4007ea2bca3..3241730a426 100644 --- a/engines/myst3/state.h +++ b/engines/myst3/state.h @@ -92,7 +92,7 @@ public: DECLARE_VAR(92, HotspotActiveRect) - DECLARE_VAR(93, WaterEffectPaused) + DECLARE_VAR(93, WaterEffectRunning) DECLARE_VAR(94, WaterEffectActive) DECLARE_VAR(95, WaterEffectSpeed) DECLARE_VAR(96, WaterEffectAttenuation)