/* ** sprite.cpp ** ** This file is part of mkxp. ** ** Copyright (C) 2013 Jonas Kulla ** ** mkxp 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. ** ** mkxp 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 mkxp. If not, see . */ #include "sprite.h" #include "sharedstate.h" #include "bitmap.h" #include "etc.h" #include "etc-internal.h" #include "util.h" #include "signals/signal.h" #include "gl-util.h" #include "quad.h" #include "transform.h" #include "shader.h" #include "glstate.h" #include "quadarray.h" #include "config.h" #include "debugwriter.h" #include "sunshine.h" #include #include #include struct SpritePrivate{ Bitmap *bitmap; Quad quad; Transform trans; Rect *srcRect; SignalConnection srcRectCon; bool mirrorX; bool mirrorY; int bushDepth; float efBushDepth; NormValue bushOpacity; NormValue opacity; BlendType blendType; IntRect sceneRect; Vec2i sceneOrig; /* Would this sprite be visible on * the screen if drawn? */ bool isVisible; bool obscured; Color *color; Tone *tone; Color *modulate; int shader; struct{ int amp; int length; int speed; float phase; /* Wave effect is active (amp != 0) */ bool active; /* qArray needs updating */ bool dirty; SimpleQuadArray qArray; } wave; EtcTemps tmp; SignalConnection prepareCon; SpritePrivate() : bitmap(0), srcRect(&tmp.rect), mirrorX(false), mirrorY(false), bushDepth(0), efBushDepth(0), bushOpacity(128), opacity(255), blendType(BlendNormal), isVisible(false), obscured(false), color(&tmp.color), tone(&tmp.tone), modulate(&tmp.modulate), shader(ShaderType::SHADER_sprite) { sceneRect.x = sceneRect.y = 0; updateSrcRectCon(); prepareCon = shState->graphicsSignals.prepareDraw.Connect(*this, &SpritePrivate::prepare); wave.amp = 0; wave.length = 180; wave.speed = 360; wave.phase = 0.0f; wave.dirty = false; } ~SpritePrivate(){ srcRectCon.Disconnect(); prepareCon.Disconnect(); } void recomputeBushDepth(){ if (nullOrDisposed(bitmap)) return; /* Calculate effective (normalized) bush depth */ float texBushDepth = (bushDepth / trans.getScale().y) - (srcRect->y + srcRect->height) + bitmap->height(); efBushDepth = 1.0f - texBushDepth / bitmap->height(); } void onSrcRectChange(){ FloatRect rect = srcRect->toFloatRect(); Vec2i bmSize; if (!nullOrDisposed(bitmap)) bmSize = Vec2i(bitmap->width(), bitmap->height()); /* Clamp the rectangle so it doesn't reach outside * the bitmap bounds */ rect.w = clamp(rect.w, 0, bmSize.x-rect.x); rect.h = clamp(rect.h, 0, bmSize.y-rect.y); quad.setTexRect(mirrorX ? rect.wFlipped() : rect); quad.setTexRect(mirrorY ? rect.hFlipped() : rect); quad.setPosRect(FloatRect(0, 0, rect.w, rect.h)); recomputeBushDepth(); wave.dirty = true; } void updateSrcRectCon(){ /* Cut old connection */ srcRectCon.Disconnect(); /* Create new one */ srcRectCon = srcRect->valueChanged.Connect(*this, &SpritePrivate::onSrcRectChange); } void updateVisibility(){ isVisible = false; if (nullOrDisposed(bitmap)) return; if (!opacity) return; if (wave.active){ /* Don't do expensive wave bounding box * calculations */ isVisible = true; return; } /* Compare sprite bounding box against the scene */ /* If sprite is zoomed/rotated, just opt out for now * for simplicity's sake */ const Vec2 &scale = trans.getScale(); if (scale.x != 1 || scale.y != 1 || trans.getRotation() != 0){ isVisible = true; return; } IntRect self; self.setPos(trans.getPositionI() - (trans.getOriginI() + sceneOrig)); self.w = bitmap->width(); self.h = bitmap->height(); isVisible = SDL_HasRectIntersection(&self, &sceneRect); } void emitWaveChunk(SVertex *&vert, float phase, int width,float zoomY, int chunkY, int chunkLength){ float wavePos = phase + (chunkY / (float) wave.length) * (float) (M_PI * 2); float chunkX = sin(wavePos) * wave.amp; FloatRect tex(0, chunkY / zoomY, width, chunkLength / zoomY); FloatRect pos = tex; pos.x = chunkX; Quad::setTexPosRect(vert, tex, pos); vert += 4; } void updateWave(){ if (nullOrDisposed(bitmap)) return; if (wave.amp == 0){ wave.active = false; return; } wave.active = true; int width = srcRect->width; int height = srcRect->height; float zoomY = trans.getScale().y; if (wave.amp < -(width / 2)){ wave.qArray.resize(0); wave.qArray.commit(); return; } /* RMVX does this, and I have no fucking clue why */ if (wave.amp < 0){ wave.qArray.resize(1); int x = -wave.amp; int w = width - x * 2; FloatRect tex(x, srcRect->y, w, srcRect->height); Quad::setTexPosRect(&wave.qArray.vertices[0], tex, tex); wave.qArray.commit(); return; } /* The length of the sprite as it appears on screen */ int visibleLength = height * zoomY; /* First chunk length (aligned to 8 pixel boundary */ int firstLength = ((int) trans.getPosition().y) % 8; /* Amount of full 8 pixel chunks in the middle */ int chunks = (visibleLength - firstLength) / 8; /* Final chunk length */ int lastLength = (visibleLength - firstLength) % 8; wave.qArray.resize(!!firstLength + chunks + !!lastLength); SVertex *vert = &wave.qArray.vertices[0]; float phase = (wave.phase * (float) M_PI) / 180.0f; if (firstLength > 0) emitWaveChunk(vert, phase, width, zoomY, 0, firstLength); for (int i = 0; i < chunks; ++i) emitWaveChunk(vert, phase, width, zoomY, firstLength + i * 8, 8); if (lastLength > 0) emitWaveChunk(vert, phase, width, zoomY, firstLength + chunks * 8, lastLength); wave.qArray.commit(); } void prepare(){ if (wave.dirty){ updateWave(); wave.dirty = false; } updateVisibility(); } }; Sprite::Sprite(Viewport *viewport) : ViewportElement(viewport){ p = new SpritePrivate; onGeometryChange(scene->getGeometry()); } Sprite::~Sprite(){ dispose(); } DEF_ATTR_RD_SIMPLE(Sprite, Bitmap, Bitmap*, p->bitmap) DEF_ATTR_RD_SIMPLE(Sprite, X, int, p->trans.getPosition().x) DEF_ATTR_RD_SIMPLE(Sprite, Y, int, p->trans.getPosition().y) DEF_ATTR_RD_SIMPLE(Sprite, OX, int, p->trans.getOrigin().x) DEF_ATTR_RD_SIMPLE(Sprite, OY, int, p->trans.getOrigin().y) DEF_ATTR_RD_SIMPLE(Sprite, ZoomX, float, p->trans.getScale().x) DEF_ATTR_RD_SIMPLE(Sprite, ZoomY, float, p->trans.getScale().y) DEF_ATTR_RD_SIMPLE(Sprite, Angle, float, p->trans.getRotation()) DEF_ATTR_RD_SIMPLE(Sprite, MirrorX, bool, p->mirrorX) DEF_ATTR_RD_SIMPLE(Sprite, MirrorY, bool, p->mirrorY) DEF_ATTR_RD_SIMPLE(Sprite, BushDepth, int, p->bushDepth) DEF_ATTR_RD_SIMPLE(Sprite, BlendType, int, p->blendType) DEF_ATTR_RD_SIMPLE(Sprite, Width, int, p->srcRect->width) DEF_ATTR_RD_SIMPLE(Sprite, Height, int, p->srcRect->height) DEF_ATTR_RD_SIMPLE(Sprite, WaveAmp, int, p->wave.amp) DEF_ATTR_RD_SIMPLE(Sprite, WaveLength, int, p->wave.length) DEF_ATTR_RD_SIMPLE(Sprite, WaveSpeed, int, p->wave.speed) DEF_ATTR_RD_SIMPLE(Sprite, WavePhase, float, p->wave.phase) // 3D shit DEF_ATTR_RD_SIMPLE(Sprite, PerspectiveMode, bool, p->trans.getPerspectiveMode()) DEF_ATTR_RD_SIMPLE(Sprite, PerspectiveZ, int, p->trans.getPerspectiveZ()) DEF_ATTR_RD_SIMPLE(Sprite, PerspectiveRotationX, float, p->trans.getPerspectiveRotation().x) DEF_ATTR_RD_SIMPLE(Sprite, PerspectiveRotationY, float, p->trans.getPerspectiveRotation().y) DEF_ATTR_SIMPLE(Sprite, BushOpacity, int, p->bushOpacity) DEF_ATTR_SIMPLE(Sprite, Opacity, int, p->opacity) DEF_ATTR_SIMPLE(Sprite, SrcRect, Rect&, *p->srcRect) DEF_ATTR_SIMPLE(Sprite, Color, Color&, *p->color) DEF_ATTR_SIMPLE(Sprite, Tone, Tone&, *p->tone) DEF_ATTR_SIMPLE(Sprite, Modulate, Color&, *p->modulate) DEF_ATTR_SIMPLE(Sprite, Obscured, bool, p->obscured) DEF_ATTR_SIMPLE(Sprite, Shader, int, p->shader) void Sprite::setBitmap(Bitmap *bitmap){ guardDisposed(); if (p->bitmap == bitmap) return; p->bitmap = bitmap; if (nullOrDisposed(bitmap)) return; bitmap->ensureNonMega(); *p->srcRect = bitmap->rect(); p->onSrcRectChange(); p->quad.setPosRect(p->srcRect->toFloatRect()); p->wave.dirty = true; } void Sprite::setX(int value){ guardDisposed(); if (p->trans.getPosition().x == value) return; p->trans.setPosition(Vec2(value, getY())); } void Sprite::setY(int value){ guardDisposed(); if (p->trans.getPosition().y == value) return; p->trans.setPosition(Vec2(getX(), value)); } void Sprite::setOX(int value){ guardDisposed(); if (p->trans.getOrigin().x == value) return; p->trans.setOrigin(Vec2(value, getOY())); } void Sprite::setOY(int value){ guardDisposed(); if (p->trans.getOrigin().y == value) return; p->trans.setOrigin(Vec2(getOX(), value)); } void Sprite::setZoomX(float value){ guardDisposed(); if (p->trans.getScale().x == value) return; p->trans.setScale(Vec2(value, getZoomY())); } void Sprite::setZoomY(float value){ guardDisposed(); if (p->trans.getScale().y == value) return; p->trans.setScale(Vec2(getZoomX(), value)); p->recomputeBushDepth(); } void Sprite::setAngle(float value){ guardDisposed(); if (p->trans.getRotation() == value) return; p->trans.setRotation(value); } void Sprite::setMirrorX(bool mirrored){ guardDisposed(); if (p->mirrorX == mirrored) return; p->mirrorX = mirrored; p->onSrcRectChange(); } void Sprite::setMirrorY(bool mirrored){ guardDisposed(); if (p->mirrorY == mirrored) return; p->mirrorY = mirrored; p->onSrcRectChange(); } void Sprite::setBushDepth(int value){ guardDisposed(); if (p->bushDepth == value) return; p->bushDepth = value; p->recomputeBushDepth(); } void Sprite::setBlendType(int type){ guardDisposed(); switch (type){ default : case BlendNormal : p->blendType = BlendNormal; return; case BlendAddition : p->blendType = BlendAddition; return; case BlendSubstraction : p->blendType = BlendSubstraction; return; case BlendMultiply : p->blendType = BlendMultiply; return; } } void Sprite::setPerspectiveMode(bool value){ guardDisposed(); if (p->trans.getPerspectiveMode() == value) return; p->trans.setPerspectiveMode(value); } void Sprite::setPerspectiveZ(int value){ guardDisposed(); if (p->trans.getPerspectiveZ() == value) return; p->trans.setPerspectiveZ(value); } void Sprite::setPerspectiveRotationX(float value){ guardDisposed(); if (p->trans.getPerspectiveRotation().x == value) return; p->trans.setPerspectiveRotation(Vec2(value, p->trans.getPerspectiveRotation().y)); } void Sprite::setPerspectiveRotationY(float value){ guardDisposed(); if (p->trans.getPerspectiveRotation().y == value) return; p->trans.setPerspectiveRotation(Vec2(p->trans.getPerspectiveRotation().x, value)); } #define DEF_WAVE_SETTER(Name, name, type) \ void Sprite::setWave##Name(type value) \ { \ guardDisposed(); \ if (p->wave.name == value) \ return; \ p->wave.name = value; \ p->wave.dirty = true; \ } DEF_WAVE_SETTER(Amp, amp, int) DEF_WAVE_SETTER(Length, length, int) DEF_WAVE_SETTER(Speed, speed, int) DEF_WAVE_SETTER(Phase, phase, float) #undef DEF_WAVE_SETTER void Sprite::initDynAttribs(){ p->srcRect = new Rect; p->color = new Color; p->tone = new Tone; p->modulate = new Color(255, 255, 255); p->updateSrcRectCon(); } /* Flashable */ void Sprite::update(){ guardDisposed(); Flashable::update(); p->wave.phase += p->wave.speed / 180; p->wave.dirty = true; } /* SceneElement */ void Sprite::draw(){ if (!p->isVisible) return; if (emptyFlashFlag) return; ShaderBase *base; bool renderEffect = p->color->hasEffect() || p->tone->hasEffect() || p->modulate->hasEffect() || flashing || p->bushDepth != 0; if (p->obscured || p->shader == ShaderType::SHADER_obscured){ ObscuredShader &shader = shState->shaders().obscured; shader.bind(); shader.setObscured(shState->graphics().obscuredTex()); base = &shader; } else{ switch (p->shader) { case ShaderType::SHADER_plane: { PlaneShader &shader = shState->shaders().plane; shader.bind(); shader.setTone(p->tone->norm); shader.setColor(p->color->norm); shader.setFlash(Vec4()); shader.setOpacity(p->opacity.norm); base = &shader; break; } case ShaderType::SHADER_water: { WaterShader &shader = shState->shaders().water; defaultSpriteShaderInit(shader); base = &shader; if (shState->sunshine().noiseBitmap) shader.setNoiseTexture(shState->sunshine().noiseBitmap->getGLTypes().tex); break; } case ShaderType::SHADER_crt: { CRTShader &shader = shState->shaders().crt; defaultSpriteShaderInit(shader); base = &shader; break; } case ShaderType::SHADER_worldMachine: { WMShader &shader = shState->shaders().worldMachine; defaultSpriteShaderInit(shader); base = &shader; break; } default: { if (renderEffect){ SpriteShader &shader = shState->shaders().sprite; defaultSpriteShaderInit(shader); base = &shader; } else if (p->opacity != 255){ AlphaSpriteShader &shader = shState->shaders().alphaSprite; shader.bind(); shader.setSpriteMat(p->trans.getMatrix()); shader.setAlpha(p->opacity.norm); base = &shader; } else{ SimpleSpriteShader &shader = shState->shaders().simpleSprite; shader.bind(); shader.setSpriteMat(p->trans.getMatrix()); base = &shader; } break; } } } boost::chrono::high_resolution_clock::time_point currentTime = boost::chrono::high_resolution_clock::now(); boost::chrono::duration elapsed = currentTime - startTime; base->setTime(elapsed.count()); base->applyViewportProj(); if(p->trans.getPerspectiveMode()) base->applyPerspectiveProj(); glState.blendMode.pushSet(p->blendType); p->bitmap->bindTex(*base); if (smooth) { //TEX::generateMipMaps(); TEX::setSmooth(true); } if (p->wave.active) p->wave.qArray.draw(); else p->quad.draw(); glState.blendMode.pop(); if (smooth) TEX::setSmooth(false); } void Sprite::onGeometryChange(const Scene::Geometry &geo){ /* Offset at which the sprite will be drawn * relative to screen origin */ p->trans.setGlobalOffset(geo.offset()); Viewport* viewport = ViewportElement::getViewport(); if (viewport) { p->trans.setGlobalScale(Vec2(viewport->getScaleX(), viewport->getScaleY())); p->trans.setGlobalRotation(viewport->getRotation()); } else { p->trans.setGlobalScale(Vec2(1.0, 1.0)); p->trans.setGlobalRotation(0); } p->sceneRect.setSize(geo.rect.size()); p->sceneOrig = geo.orig; } void Sprite::releaseResources(){ unlink(); delete p; } void Sprite::defaultSpriteShaderInit(SpriteShaderBase &shader){ shader.bind(); shader.setSpriteMat(p->trans.getMatrix()); shader.setTone(p->tone->norm); shader.setOpacity(p->opacity.norm); shader.setBushDepth(p->efBushDepth); shader.setBushOpacity(p->bushOpacity.norm); /* When both flashing and effective color are set, * the one with higher alpha will be blended */ const Vec4 *blend = (flashing && flashColor.w > p->color->norm.w) ? &flashColor : &p->color->norm; shader.setColor(*blend); shader.setModulate(p->modulate->norm); }