diff --git a/data/themes/MegaLight V4/stages/default.png b/data/themes/MegaLight V4/backgrounds/default.png similarity index 100% rename from data/themes/MegaLight V4/stages/default.png rename to data/themes/MegaLight V4/backgrounds/default.png diff --git a/data/themes/MegaLight V4/hitflamesanimation.png.bak b/data/themes/MegaLight V4/hitflamesanimation.png.bak new file mode 100644 index 000000000..78a1e5211 Binary files /dev/null and b/data/themes/MegaLight V4/hitflamesanimation.png.bak differ diff --git a/data/themes/MegaLight V4/stage.ini b/data/themes/MegaLight V4/stage.ini index 8b1378917..35f810d7c 100644 --- a/data/themes/MegaLight V4/stage.ini +++ b/data/themes/MegaLight V4/stage.ini @@ -1 +1,258 @@ +[layer1] +texture = stage_background.png +xres = 512 +yres = 512 +xscale = 1.3 +yscale = 1.3 +xpos = 0.0 +ypos = 0.0 +angle = 0.0 +[layer2] +texture = stage_lights1.png +xres = 256 +yres = 128 +xpos = -0.65 +ypos = -0.8 + +[layer3] +texture = stage_lights2.png +xres = 256 +yres = 128 +xpos = 0.65 +ypos = -0.8 + +[layer4] +texture = stage_drums.png +xres = 256 +yres = 256 +xpos = -0.7 +ypos = 0.0 + +[layer4:fx1] +type = scale +trigger = beat +xmagnitude = 0.01 +ymagnitude = 0.01 + +[layer5] +texture = stage_bassdrum.png +xres = 128 +yres = 128 +xpos = -0.65 +ypos = 0.13 + +[layer5:fx1] +type = scale +trigger = beat +xmagnitude = 0.06 +ymagnitude = 0.06 + +[layer6] +texture = stage_speakers.png +xres = 256 +yres = 256 +xpos = 0.7 +ypos = 0.0 + +[layer6:fx1] +type = scale +trigger = beat +xmagnitude = 0.03 +ymagnitude = 0.03 +delay = 0.5 + +[layer7] +texture = stage_speaker_cones.png +xres = 256 +yres = 256 +xpos = 0.67 +ypos = -0.03 + +[layer7:fx1] +type = scale +trigger = beat +xmagnitude = 0.07 +ymagnitude = 0.07 + +[layer8] +texture = stage_audience1.png +xres = 256 +yres = 256 +xpos = -0.8 +ypos = 0.4 + +[layer8:fx1] +type = wiggle +trigger = beat +xmagnitude = 0.02 +ymagnitude = -0.04 +frequency = 0.5 + +[layer9] +texture = stage_audience2.png +xres = 256 +yres = 256 +xpos = 0.8 +ypos = 0.4 + +[layer9:fx1] +type = wiggle +trigger = beat +xmagnitude = 0.01 +ymagnitude = -0.03 +frequency = 1.0 +delay = .25 + +[layer10] +texture = stage_light.png +xres = 256 +yres = 256 +xpos = -0.87 +ypos = -0.75 +xscale = 3.0 +yscale = 3.0 +src_blending = src_alpha +dst_blending = one +foreground = 1 + +[layer10:fx1] +type = light +trigger = pick +light_number = 0 +intensity = 0.7 + +[layer10:fx2] +type = rotate +trigger = miss +profile = sinstep +angle = 10 +period = 75 + +[layer11] +texture = stage_light.png +xres = 256 +yres = 256 +xpos = -0.69 +ypos = -0.8 +xscale = 3.0 +yscale = 3.0 +angle = -12.0 +src_blending = src_alpha +dst_blending = one +foreground = 1 + +[layer11:fx1] +type = light +trigger = pick +light_number = 1 +intensity = 0.7 + +[layer11:fx2] +type = rotate +trigger = miss +profile = sinstep +angle = -10 +period = 100 + +[layer12] +texture = stage_light.png +xres = 256 +yres = 256 +xpos = -0.48 +ypos = -0.83 +xscale = 3.0 +yscale = 3.0 +angle = -16.0 +src_blending = src_alpha +dst_blending = one +foreground = 1 + +[layer12:fx1] +type = light +trigger = pick +light_number = 2 +intensity = 0.7 + +[layer12:fx2] +type = rotate +trigger = miss +profile = sinstep +angle = 7 +period = 150 + +[layer13] +texture = stage_light.png +xres = 256 +yres = 256 +xpos = 0.83 +ypos = -0.75 +xscale = 3.0 +yscale = 3.0 +src_blending = src_alpha +dst_blending = one +foreground = 1 + +[layer13:fx1] +type = light +trigger = pick +intensity = 0.7 +light_number = 0 + +[layer13:fx2] +type = rotate +trigger = miss +profile = sinstep +angle = 12 +period = 250 + +[layer14] +texture = stage_light.png +xres = 256 +yres = 256 +xpos = 0.64 +ypos = -0.84 +xscale = 3.0 +yscale = 3.0 +angle = 12.0 +src_blending = src_alpha +dst_blending = one +foreground = 1 + +[layer14:fx1] +type = light +trigger = pick +light_number = 1 +intensity = 0.7 + +[layer14:fx2] +type = rotate +trigger = miss +profile = sinstep +angle = -5 +period = 200 + +[layer15] +texture = stage_light.png +xres = 256 +yres = 256 +xpos = 0.46 +ypos = -0.9 +xscale = 3.0 +yscale = 3.0 +angle = 19.0 +src_blending = src_alpha +dst_blending = one +foreground = 1 + +[layer15:fx1] +type = light +trigger = pick +light_number = 2 +intensity = 0.7 + +[layer15:fx2] +type = rotate +trigger = miss +angle = -5 +period = 150 diff --git a/data/themes/MegaLight V4/stage/light.png b/data/themes/MegaLight V4/stage/light.png new file mode 100644 index 000000000..9e562751f Binary files /dev/null and b/data/themes/MegaLight V4/stage/light.png differ diff --git a/data/themes/MegaLight V4/stage/stage_audience1.png b/data/themes/MegaLight V4/stage/stage_audience1.png new file mode 100644 index 000000000..5fc96f60c Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_audience1.png differ diff --git a/data/themes/MegaLight V4/stage/stage_audience2.png b/data/themes/MegaLight V4/stage/stage_audience2.png new file mode 100644 index 000000000..b7441c017 Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_audience2.png differ diff --git a/data/themes/MegaLight V4/stage/stage_background.png b/data/themes/MegaLight V4/stage/stage_background.png new file mode 100644 index 000000000..c63c5c6ef Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_background.png differ diff --git a/data/themes/MegaLight V4/stage/stage_bassdrum.png b/data/themes/MegaLight V4/stage/stage_bassdrum.png new file mode 100644 index 000000000..fbacdbe4a Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_bassdrum.png differ diff --git a/data/themes/MegaLight V4/stage/stage_drums.png b/data/themes/MegaLight V4/stage/stage_drums.png new file mode 100644 index 000000000..9835e1701 Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_drums.png differ diff --git a/data/themes/MegaLight V4/stage/stage_light.png b/data/themes/MegaLight V4/stage/stage_light.png new file mode 100644 index 000000000..c87cd427c Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_light.png differ diff --git a/data/themes/MegaLight V4/stage/stage_lights1.png b/data/themes/MegaLight V4/stage/stage_lights1.png new file mode 100644 index 000000000..6828ff76c Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_lights1.png differ diff --git a/data/themes/MegaLight V4/stage/stage_lights2.png b/data/themes/MegaLight V4/stage/stage_lights2.png new file mode 100644 index 000000000..f77ee00f6 Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_lights2.png differ diff --git a/data/themes/MegaLight V4/stage/stage_speaker_cones.png b/data/themes/MegaLight V4/stage/stage_speaker_cones.png new file mode 100644 index 000000000..fed6a419c Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_speaker_cones.png differ diff --git a/data/themes/MegaLight V4/stage/stage_speakers.png b/data/themes/MegaLight V4/stage/stage_speakers.png new file mode 100644 index 000000000..2704c7a10 Binary files /dev/null and b/data/themes/MegaLight V4/stage/stage_speakers.png differ diff --git a/src/Stage.py b/src/Stage.py index cedbf10b6..e877f4ccd 100644 --- a/src/Stage.py +++ b/src/Stage.py @@ -32,6 +32,7 @@ import os import random #MFH - needed for new stage background handling from Language import _ +import math try: from VideoPlayer import VideoPlayer @@ -41,12 +42,208 @@ import Rockmeter #blazingamer - new 4.0 code for rendering rockmeters through stage.ini + +class Layer(object): + """ + A graphical stage layer that can have a number of animation effects associated with it. + """ + def __init__(self, stage, drawing): + """ + Constructor. + + @param stage: Containing Stage + @param drawing: SvgDrawing for this layer. Make sure this drawing is rendered to + a texture for performance reasons. + """ + self.stage = stage + self.drawing = drawing + self.position = (0.0, 0.0) + self.angle = 0.0 + self.scale = (1.0, 1.0) + self.color = (1.0, 1.0, 1.0, 1.0) + self.srcBlending = GL_SRC_ALPHA + self.dstBlending = GL_ONE_MINUS_SRC_ALPHA + self.effects = [] + + def render(self, visibility): + """ + Render the layer. + + @param visibility: Floating point visibility factor (1 = opaque, 0 = invisibile) + """ + w, h, = self.stage.engine.view.geometry[2:4] + v = 1.0 - visibility ** 2 + + color = self.color + coord = [self.position[0] * w / 2, -self.position[1] * h / 2] + if v > .01: + color = (self.color[0], self.color[1], self.color[2], visibility) + if self.position[0] < -.25: + coord[0] *= -v * w + elif self.position[0] > .25: + coord[0] *= v * w + scale = [self.scale[0], -self.scale[1]] + rot = self.angle + + self.stage.engine.drawImage(self.drawing, scale, coord, rot, color) + + # Blend in all the effects + for effect in self.effects: + effect.apply() + +class Effect(object): + """ + An animationn effect that can be attached to a Layer. + """ + def __init__(self, layer, options): + """ + Constructor. + + @param layer: Layer to attach this effect to. + @param options: Effect options (default in parens): + intensity - Floating point effect intensity (1.0) + trigger - Effect trigger, one of "none", "beat", + "quarterbeat", "pick", "miss" ("none") + period - Trigger period in ms (200.0) + delay - Trigger delay in periods (0.0) + profile - Trigger profile, one of "step", "linstep", + "smoothstep" + """ + self.layer = layer + self.stage = layer.stage + self.intensity = float(options.get("intensity", 1.0)) + self.trigger = getattr(self, "trigger" + options.get("trigger", "none").capitalize()) + self.period = float(options.get("period", 500.0)) + self.delay = float(options.get("delay", 0.0)) + self.triggerProf = getattr(self, options.get("profile", "linstep")) + + def apply(self): + pass + + def triggerNone(self): + return 0.0 + + def triggerBeat(self): + if not self.stage.lastBeatPos: + return 0.0 + t = self.stage.pos - self.delay * self.stage.beatPeriod - self.stage.lastBeatPos + return self.intensity * (1.0 - self.triggerProf(0, self.stage.beatPeriod, t)) + + def triggerQuarterbeat(self): + if not self.stage.lastQuarterBeatPos: + return 0.0 + t = self.stage.pos - self.delay * (self.stage.beatPeriod / 4) - self.stage.lastQuarterBeatPos + return self.intensity * (1.0 - self.triggerProf(0, self.stage.beatPeriod / 4, t)) + + def triggerPick(self): + if not self.stage.lastPickPos: + return 0.0 + t = self.stage.pos - self.delay * self.period - self.stage.lastPickPos + return self.intensity * (1.0 - self.triggerProf(0, self.period, t)) + + def triggerMiss(self): + if not self.stage.lastMissPos: + return 0.0 + t = self.stage.pos - self.delay * self.period - self.stage.lastMissPos + return self.intensity * (1.0 - self.triggerProf(0, self.period, t)) + + def step(self, threshold, x): + return (x > threshold) and 1 or 0 + + def linstep(self, min, max, x): + if x < min: + return 0 + if x > max: + return 1 + return (x - min) / (max - min) + + def smoothstep(self, min, max, x): + if x < min: + return 0 + if x > max: + return 1 + def f(x): + return -2 * x**3 + 3*x**2 + return f((x - min) / (max - min)) + + def sinstep(self, min, max, x): + return math.cos(math.pi * (1.0 - self.linstep(min, max, x))) + + def getNoteColor(self, note): + if note >= len(self.stage.engine.theme.fretColors) - 1: + return self.stage.engine.theme.fretColors[-1] + elif note <= 0: + return self.stage.engine.theme.fretColors[0] + f2 = note % 1.0 + f1 = 1.0 - f2 + c1 = self.stage.engine.theme.fretColors[int(note)] + c2 = self.stage.engine.theme.fretColors[int(note) + 1] + return (c1[0] * f1 + c2[0] * f2, \ + c1[1] * f1 + c2[1] * f2, \ + c1[2] * f1 + c2[2] * f2) + +class LightEffect(Effect): + def __init__(self, layer, options): + Effect.__init__(self, layer, options) + self.lightNumber = int(options.get("light_number", 0)) + self.ambient = float(options.get("ambient", 0.5)) + self.contrast = float(options.get("contrast", 0.5)) + + def apply(self): + if len(self.stage.averageNotes) < self.lightNumber + 2: + self.layer.color = (0.0, 0.0, 0.0, 0.0) + return + + t = self.trigger() + t = self.ambient + self.contrast * t + c = self.getNoteColor(self.stage.averageNotes[self.lightNumber]) + self.layer.setColor((c[0] * t, c[1] * t, c[2] * t, self.intensity)) + +class RotateEffect(Effect): + def __init__(self, layer, options): + Effect.__init__(self, layer, options) + self.angle = math.pi / 180.0 * float(options.get("angle", 45)) + + def apply(self): + if not self.stage.lastMissPos: + return + + t = self.trigger() + self.layer.drawing.setAngle(t * self.angle) + +class WiggleEffect(Effect): + def __init__(self, layer, options): + Effect.__init__(self, layer, options) + self.freq = float(options.get("frequency", 6)) + self.xmag = float(options.get("xmagnitude", 0.1)) + self.ymag = float(options.get("ymagnitude", 0.1)) + + def apply(self): + t = self.trigger() + + w, h = self.stage.engine.view.geometry[2:4] + p = t * 2 * math.pi * self.freq + s, c = t * math.sin(p), t * math.cos(p) + self.layer.drawing.setPosition(self.xmag * w * s, self.ymag * h * c) + +class ScaleEffect(Effect): + def __init__(self, layer, options): + Effect.__init__(self, layer, options) + self.xmag = float(options.get("xmagnitude", .1)) + self.ymag = float(options.get("ymagnitude", .1)) + + def apply(self): + t = self.trigger() + self.layer.drawing.setScale(1.0 + self.xmag * t, 1.0 + self.ymag * t) + class Stage(object): def __init__(self, guitarScene, configFileName): self.scene = guitarScene self.engine = guitarScene.engine self.config = Config.MyConfigParser() - self.layers = [] + self.backgroundLayers = [] + self.foregroundLayers = [] + self.textures = {} self.reset() @@ -85,13 +282,74 @@ def __init__(self, guitarScene, configFileName): # evilynux - Improved stage error handling self.themename = self.engine.data.themeLabel - self.path = os.path.join("themes",self.themename,"stages") + self.path = os.path.join("themes",self.themename,"backgrounds") self.pathfull = self.engine.getPath(self.path) if not os.path.exists(self.pathfull): # evilynux Log.warn("Stage folder does not exist: %s" % self.pathfull) self.mode = 1 # Fallback to song-specific stage suffix = ".jpg" + self.loadLayers(configFileName) + + def loadLayers(self, configFileName): + self.config.read(configFileName) + path = os.path.join("themes", self.themename, "stage") + + # Build the layers + for i in range(32): + section = "layer%d" % i + if self.config.has_section(section): + def get(value, type = str, default = None): + if self.config.has_option(section, value): + return type(self.config.get(section, value)) + return default + + xres = get("xres", int, 256) + yres = get("yres", int, 256) + texture = get("texture") + + try: + drawing = self.textures[texture] + except KeyError: + drawing = self.engine.loadImgDrawing(self, None, os.path.join(path, texture), textureSize = (xres, yres)) + self.textures[texture] = drawing + + layer = Layer(self, drawing) + + layer.position = (get("xpos", float, 0.0), get("ypos", float, 0.0)) + layer.scale = (get("xscale", float, 1.0), get("yscale", float, 1.0)) + layer.angle = math.pi * get("angle", float, 0.0) / 180.0 + layer.srcBlending = globals()["GL_%s" % get("src_blending", str, "src_alpha").upper()] + layer.dstBlending = globals()["GL_%s" % get("dst_blending", str, "one_minus_src_alpha").upper()] + layer.color = (get("color_r", float, 1.0), get("color_g", float, 1.0), get("color_b", float, 1.0), get("color_a", float, 1.0)) + + # Load any effects + fxClasses = { + "light": LightEffect, + "rotate": RotateEffect, + "wiggle": WiggleEffect, + "scale": ScaleEffect, + } + + for j in range(32): + fxSection = "layer%d:fx%d" % (i, j) + if self.config.has_section(fxSection): + type = self.config.get(fxSection, "type") + + if not type in fxClasses: + continue + + options = self.config.options(fxSection) + options = dict([(opt, self.config.get(fxSection, opt)) for opt in options]) + + fx = fxClasses[type](layer, options) + layer.effects.append(fx) + + if get("foreground", int): + self.foregroundLayers.append(layer) + else: + self.backgroundLayers.append(layer) + def loadVideo(self, libraryName, songName, songVideo = None, songVideoStartTime = None, songVideoEndTime = None): if not videoAvailable: @@ -336,11 +594,6 @@ def triggerBeat(self, pos, beat): self.beat = beat self.averageNotes = self.averageNotes[-4:] + self.averageNotes[-1:] - def renderLayers(self, layers, visibility): - with self.engine.view.orthogonalProjection(normalize = True): - for layer in layers: - layer.render(visibility) - def run(self, pos, period): self.pos = pos self.beatPeriod = period @@ -354,10 +607,15 @@ def run(self, pos, period): if beat > self.beat: self.triggerBeat(pos, beat) + def renderLayers(self, layers, visibility): + with self.engine.view.orthogonalProjection(normalize = True): + for layer in layers: + layer.render(visibility) + def render(self, visibility): if self.mode != 3: self.renderBackground() - self.renderLayers(self.layers, visibility) + self.renderLayers(self.backgroundLayers, visibility) if shaders.enable("stage"): height = 0.0 for i in shaders.var["color"].keys(): @@ -377,6 +635,5 @@ def render(self, visibility): shaders.disable() self.scene.renderGuitar() + self.renderLayers(self.foregroundLayers, visibility) self.rockmeter.render(visibility) - -