From 39bc43eeefdb6dbff83448574c5215fd8dbe1d8e Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Mon, 7 Aug 2023 18:40:00 -0700 Subject: [PATCH] recreate the old effect using shaders (#317) * recreate the old effect using shaders * blue tint on shadows --- Effects/DynamicShadows/source/Main.hx | 3 +- Effects/DynamicShadows/source/PlayState.hx | 146 +++------------ .../DynamicShadows/source/PlayStateFlash.hx | 175 ++++++++++++++++++ .../DynamicShadows/source/PlayStateShader.hx | 43 +++++ Effects/DynamicShadows/source/Shader.hx | 111 +++++++++++ 5 files changed, 357 insertions(+), 121 deletions(-) create mode 100644 Effects/DynamicShadows/source/PlayStateFlash.hx create mode 100644 Effects/DynamicShadows/source/PlayStateShader.hx create mode 100644 Effects/DynamicShadows/source/Shader.hx diff --git a/Effects/DynamicShadows/source/Main.hx b/Effects/DynamicShadows/source/Main.hx index 7568f78a7..3d44bec01 100644 --- a/Effects/DynamicShadows/source/Main.hx +++ b/Effects/DynamicShadows/source/Main.hx @@ -8,6 +8,7 @@ class Main extends Sprite public function new() { super(); - addChild(new FlxGame(640, 320, PlayState)); + + addChild(new FlxGame(640, 320, #if flash PlayStateFlash #else PlayStateShader #end)); } } diff --git a/Effects/DynamicShadows/source/PlayState.hx b/Effects/DynamicShadows/source/PlayState.hx index 8544048cd..c193aa681 100644 --- a/Effects/DynamicShadows/source/PlayState.hx +++ b/Effects/DynamicShadows/source/PlayState.hx @@ -1,5 +1,6 @@ package; +import flixel.FlxCamera; import flixel.addons.nape.FlxNapeSpace; import flixel.addons.nape.FlxNapeTilemap; import flixel.FlxG; @@ -19,7 +20,7 @@ using flixel.util.FlxSpriteUtil; /** * This was based on a guide from this forum post: http://forums.tigsource.com/index.php?topic=8803.0 - * Ported to HaxeFlixel by Xerosugar + * Ported to HaxeFlixel by Xerosugar, then converted to use Shaders by GeoKureli * * If you're feeling up the challenge, here's how YOU help can improve this demo: * - Make it possible to extends the shadows to the edge of the screen @@ -35,9 +36,6 @@ class PlayState extends FlxState { public static inline var TILE_SIZE:Int = 16; - static inline var SHADOW_COLOR = 0xff2a2963; - static inline var OVERLAY_COLOR = 0xff887fff; - /** * Only contains non-collidabe tiles */ @@ -47,28 +45,17 @@ class PlayState extends FlxState * The layer into which the actual "level" will be drawn, and also the one objects will collide with */ var foreground:FlxNapeTilemap; - + /** - * The sprite that shadows will be drawn to + * Anything you want to show above the shadows */ - var shadowCanvas:FlxSprite; - - /** - * The sprite that the actual darkness and the gem's flare-like effect will be drawn to - */ - var shadowOverlay:FlxSprite; - + var uiCam:FlxCamera; + /** * The light source! */ var gem:Gem; - /** - * If there's a small gap between something (could be two tiles, - * even if they're right next to each other), this should cover it up for us - */ - var lineStyle:LineStyle = {color: SHADOW_COLOR, thickness: 1}; - var infoText:FlxText; var fps:FPS; @@ -76,28 +63,14 @@ class PlayState extends FlxState { super.create(); - FlxG.camera.bgColor = 0x5a81ad; - FlxNapeSpace.init(); FlxNapeSpace.space.gravity.setxy(0, 1200); FlxNapeSpace.drawDebug = false; // You can toggle this on/off one by pressing 'D' - var background:FlxTilemap = new FlxTilemap(); + background = new FlxTilemap(); background.loadMapFromCSV("assets/data/background.txt", "assets/images/tiles.png", TILE_SIZE, TILE_SIZE, null, 1, 1); add(background); - // Note: The tilemap used in this demo was drawn with 'Tiled' (http://www.mapeditor.org/), - // but the level data was picked from the .tmx file and put into two separate - // .txt files for simplicity. If you wish to learn how to use Tiled with your project, - // have a look at this demo: http://haxeflixel.com/demos/TiledEditor/ - - // If we add the shadows *before* all of the foreground elements (stage included) - // they will only cover the background, which is usually what you'd want I'd guess :) - shadowCanvas = new FlxSprite(); - shadowCanvas.blend = BlendMode.MULTIPLY; - shadowCanvas.makeGraphic(FlxG.width, FlxG.height, FlxColor.TRANSPARENT, true); - add(shadowCanvas); - foreground = new FlxNapeTilemap(); foreground.loadMapFromCSV("assets/data/foreground.txt", "assets/images/tiles.png", TILE_SIZE, TILE_SIZE, null, 1, 1); add(foreground); @@ -105,11 +78,6 @@ class PlayState extends FlxState foreground.setupTileIndices([4]); createProps(); - shadowOverlay = new FlxSprite(); - shadowOverlay.makeGraphic(FlxG.width, FlxG.height, FlxColor.TRANSPARENT, true); - shadowOverlay.blend = BlendMode.MULTIPLY; - add(shadowOverlay); - infoText = new FlxText(10, 10, 100, ""); add(infoText); @@ -117,6 +85,22 @@ class PlayState extends FlxState fps = new FPS(10, 10, 0xffffff); FlxG.stage.addChild(fps); fps.visible = false; + + createCams(); + } + + function createCams() + { + // FlxG.camera draws the actual world. In this case, that means everything except infoText + FlxG.camera.bgColor = 0x5a81ad; + gem.camera = FlxG.camera; + background.camera = FlxG.camera; + + // draws anything above the sahdows, in this case infoText + uiCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); + FlxG.cameras.add(uiCam, false); + uiCam.bgColor = 0x0; + infoText.camera = uiCam; } function createProps():Void @@ -154,6 +138,8 @@ class PlayState extends FlxState override public function update(elapsed:Float):Void { + super.update(elapsed); + infoText.text = "FPS: " + fps.currentFPS + "\n\nObjects can be dragged/thrown around.\n\nPress 'R' to restart."; if (FlxG.keys.justPressed.R) @@ -161,90 +147,10 @@ class PlayState extends FlxState if (FlxG.keys.justPressed.D) FlxNapeSpace.drawDebug = !FlxNapeSpace.drawDebug; - - processShadows(); - super.update(elapsed); - } - - public function processShadows():Void - { - shadowCanvas.fill(FlxColor.TRANSPARENT); - shadowOverlay.fill(OVERLAY_COLOR); - - shadowOverlay.drawCircle( // outer red circle - gem.body.position.x + FlxG.random.float(-.6, .6), gem.body.position.y + FlxG.random.float(-.6, .6), - (FlxG.random.bool(5) ? 16 : 16.5), 0xffff5f5f); - - shadowOverlay.drawCircle( // inner red circle - gem.body.position.x + FlxG.random.float(-.25, .25), gem.body.position.y + FlxG.random.float(-.25, .25), - (FlxG.random.bool(5) ? 13 : 13.5), 0xffff7070); - - for (body in FlxNapeSpace.space.bodies) - { - // We don't want to draw any shadows around the gem, since it's the light source - if (body.userData.type != "Gem") - processBodyShapes(body); - } - } - - function processBodyShapes(body:Body) - { - for (shape in body.shapes) - { - var verts:Vec2List = shape.castPolygon.worldVerts; - - for (i in 0...verts.length) - { - var startVertex:Vec2 = (i == 0) ? verts.at(verts.length - 1) : verts.at(i - 1); - processShapeVertex(startVertex, verts.at(i)); - } - } - } - - function processShapeVertex(startVertex:Vec2, endVertex:Vec2):Void - { - var tempLightOrigin:Vec2 = Vec2.get(gem.body.position.x + FlxG.random.float(-.3, 3), gem.body.position.y + FlxG.random.float(-.3, .3)); - - if (doesEdgeCastShadow(startVertex, endVertex, tempLightOrigin)) - { - var projectedPoint:Vec2 = projectPoint(startVertex, tempLightOrigin); - var prevProjectedPt:Vec2 = projectPoint(endVertex, tempLightOrigin); - var vts:Array = [ - FlxPoint.weak(startVertex.x, startVertex.y), - FlxPoint.weak(projectedPoint.x, projectedPoint.y), - FlxPoint.weak(prevProjectedPt.x, prevProjectedPt.y), - FlxPoint.weak(endVertex.x, endVertex.y) - ]; - - shadowCanvas.drawPolygon(vts, SHADOW_COLOR, lineStyle); - } - } - - function projectPoint(point:Vec2, light:Vec2):Vec2 - { - var lightToPoint:Vec2 = point.copy(); - lightToPoint.subeq(light); - - var projectedPoint:Vec2 = point.copy(); - return projectedPoint.addeq(lightToPoint.muleq(.45)); - } - - function doesEdgeCastShadow(start:Vec2, end:Vec2, light:Vec2):Bool - { - var startToEnd:Vec2 = end.copy(); - startToEnd.subeq(start); - - var normal:Vec2 = new Vec2(startToEnd.y, -1 * startToEnd.x); - - var lightToStart:Vec2 = start.copy(); - lightToStart.subeq(light); - - return normal.dot(lightToStart) > 0; } } -@:enum -abstract Prop(Int) to Int +enum abstract Prop(Int) to Int { var BARREL = 5; var GEM = 6; diff --git a/Effects/DynamicShadows/source/PlayStateFlash.hx b/Effects/DynamicShadows/source/PlayStateFlash.hx new file mode 100644 index 000000000..590f757bc --- /dev/null +++ b/Effects/DynamicShadows/source/PlayStateFlash.hx @@ -0,0 +1,175 @@ +package; + +import flixel.addons.nape.FlxNapeSpace; +import flixel.addons.nape.FlxNapeTilemap; +import flixel.FlxCamera; +import flixel.FlxG; +import flixel.FlxSprite; +import flixel.FlxState; +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import flixel.tile.FlxTilemap; +import flixel.util.FlxColor; +import nape.geom.Vec2; +import nape.geom.Vec2List; +import nape.phys.Body; +import openfl.display.BlendMode; +import openfl.display.FPS; + +using flixel.util.FlxSpriteUtil; + +/** + * The old version of dynamic shadows, where the shadows are drawn via openfl.display.graphics. + * kept for no good reason other than posterity + */ +class PlayStateFlash extends PlayState +{ + public static inline var TILE_SIZE:Int = PlayState.TILE_SIZE; + + static inline var SHADOW_COLOR = 0xff2a2963; + static inline var OVERLAY_COLOR = 0xff887fff; + + /** + * Camera containing the casted shadows + */ + var shadowCam:FlxCamera; + + /** + * The sprite that shadows will be drawn to + */ + var shadowCanvas:FlxSprite; + + /** + * The sprite that the actual darkness and the gem's flare-like effect will be drawn to + */ + var shadowOverlay:FlxSprite; + + /** + * If there's a small gap between something (could be two tiles, + * even if they're right next to each other), this should cover it up for us + */ + var lineStyle:LineStyle = {color: SHADOW_COLOR, thickness: 1}; + + override function createCams() + { + // Note: The tilemap used in this demo was drawn with 'Tiled' (http://www.mapeditor.org/), + // but the level data was picked from the .tmx file and put into two separate + // .txt files for simplicity. If you wish to learn how to use Tiled with your project, + // have a look at this demo: http://haxeflixel.com/demos/TiledEditor/ + + // If we add the shadows *before* all of the foreground elements (stage included) + // they will only cover the background, which is usually what you'd want I'd guess :) + shadowCanvas = new FlxSprite(); + shadowCanvas.blend = BlendMode.MULTIPLY; + shadowCanvas.makeGraphic(FlxG.width, FlxG.height, FlxColor.TRANSPARENT, true); + var index = members.indexOf(foreground); + // place behind foreground + insert(index, shadowCanvas); + + shadowOverlay = new FlxSprite(); + shadowOverlay.makeGraphic(FlxG.width, FlxG.height, FlxColor.TRANSPARENT, true); + shadowOverlay.blend = BlendMode.MULTIPLY; + add(shadowOverlay); + + // FlxG.camera draws the actual world. In this case, that means everything except infoText + FlxG.camera.bgColor = 0x5a81ad; + gem.camera = FlxG.camera; + background.camera = FlxG.camera; + + // places the casted shadows above the foreground and below the ui + // shadowCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); + // FlxG.cameras.add(shadowCam, false); + // shadowCam.bgColor = 0x0; + // shadowCanvas.camera = shadowCam; + // shadowOverlay.camera = shadowCam; + + // draws anything above the sahdows, in this case infoText + uiCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); + FlxG.cameras.add(uiCam, false); + uiCam.bgColor = 0x0; + infoText.camera = uiCam; + } + + override public function update(elapsed:Float):Void + { + super.update(elapsed); + + processShadows(); + } + + public function processShadows():Void + { + shadowCanvas.fill(FlxColor.TRANSPARENT); + shadowOverlay.fill(OVERLAY_COLOR); + + shadowOverlay.drawCircle( // outer red circle + gem.body.position.x + FlxG.random.float(-.6, .6), gem.body.position.y + FlxG.random.float(-.6, .6), + (FlxG.random.bool(5) ? 16 : 16.5), 0xffff5f5f); + + shadowOverlay.drawCircle( // inner red circle + gem.body.position.x + FlxG.random.float(-.25, .25), gem.body.position.y + FlxG.random.float(-.25, .25), + (FlxG.random.bool(5) ? 13 : 13.5), 0xffff7070); + + for (body in FlxNapeSpace.space.bodies) + { + // We don't want to draw any shadows around the gem, since it's the light source + if (body.userData.type != "Gem") + processBodyShapes(body); + } + } + + function processBodyShapes(body:Body) + { + for (shape in body.shapes) + { + var verts:Vec2List = shape.castPolygon.worldVerts; + + for (i in 0...verts.length) + { + var startVertex:Vec2 = (i == 0) ? verts.at(verts.length - 1) : verts.at(i - 1); + processShapeVertex(startVertex, verts.at(i)); + } + } + } + + function processShapeVertex(startVertex:Vec2, endVertex:Vec2):Void + { + var tempLightOrigin:Vec2 = Vec2.get(gem.body.position.x + FlxG.random.float(-.3, 3), gem.body.position.y + FlxG.random.float(-.3, .3)); + + if (doesEdgeCastShadow(startVertex, endVertex, tempLightOrigin)) + { + var projectedPoint:Vec2 = projectPoint(startVertex, tempLightOrigin); + var prevProjectedPt:Vec2 = projectPoint(endVertex, tempLightOrigin); + var vts:Array = [ + FlxPoint.weak(startVertex.x, startVertex.y), + FlxPoint.weak(projectedPoint.x, projectedPoint.y), + FlxPoint.weak(prevProjectedPt.x, prevProjectedPt.y), + FlxPoint.weak(endVertex.x, endVertex.y) + ]; + + shadowCanvas.drawPolygon(vts, SHADOW_COLOR, lineStyle); + } + } + + function projectPoint(point:Vec2, light:Vec2):Vec2 + { + var lightToPoint:Vec2 = point.copy(); + lightToPoint.subeq(light); + + var projectedPoint:Vec2 = point.copy(); + return projectedPoint.addeq(lightToPoint.muleq(.45)); + } + + function doesEdgeCastShadow(start:Vec2, end:Vec2, light:Vec2):Bool + { + var startToEnd:Vec2 = end.copy(); + startToEnd.subeq(start); + + var normal:Vec2 = new Vec2(startToEnd.y, -1 * startToEnd.x); + + var lightToStart:Vec2 = start.copy(); + lightToStart.subeq(light); + + return normal.dot(lightToStart) > 0; + } +} diff --git a/Effects/DynamicShadows/source/PlayStateShader.hx b/Effects/DynamicShadows/source/PlayStateShader.hx new file mode 100644 index 000000000..deb543828 --- /dev/null +++ b/Effects/DynamicShadows/source/PlayStateShader.hx @@ -0,0 +1,43 @@ +import flixel.FlxCamera; +import flixel.FlxG; +class PlayStateShader extends PlayState +{ + var shaderCam:FlxCamera; + + var shader:Shader; + + override function create():Void + { + super.create(); + } + + override function createCams() + { + // FlxG.camera draws the actual world. In this case, that means the background + FlxG.camera.bgColor = 0x5a81ad; + gem.camera = FlxG.camera; + background.camera = FlxG.camera; + FlxG.cameras.setDefaultDrawTarget(FlxG.camera, false); + + // shaderCam draws casted shadows from everything drawn to it, these draw above FlxG.camera + // In this case that means everything except ui and the background + shaderCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); + FlxG.cameras.add(shaderCam); + shaderCam.bgColor = 0x0; + shader = new Shader(); + shaderCam.setFilters([new openfl.filters.ShaderFilter(shader)]); + + // draws anything above the sahdows, in this case infoText + uiCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); + FlxG.cameras.add(uiCam, false); + uiCam.bgColor = 0x0; + infoText.camera = uiCam; + } + + override function update(elapsed:Float) + { + super.update(elapsed); + + shader.setOrigin((gem.x + gem.origin.x) / FlxG.width, (gem.y + gem.origin.y) / FlxG.height); + } +} \ No newline at end of file diff --git a/Effects/DynamicShadows/source/Shader.hx b/Effects/DynamicShadows/source/Shader.hx new file mode 100644 index 000000000..8ab10cc30 --- /dev/null +++ b/Effects/DynamicShadows/source/Shader.hx @@ -0,0 +1,111 @@ +package; + +import flixel.FlxG; +import flixel.math.FlxPoint; + +class Shader extends flixel.system.FlxAssets.FlxShader +{ + public var originX(get, never):Float; + inline function get_originX() return this.uOrigin.value[0]; + + public var originY(get, never):Float; + inline function get_originY() return this.uOrigin.value[1]; + + public var shade(get, set):Float; + inline function get_shade() return this.uShade.value[0]; + inline function set_shade(value:Float) + { + this.uShade.value = [Math.min(1, Math.max(0, value))]; + FlxG.watch.addQuick("shade", value); + return value; + } + + public var glowRadius(get, set):Float; + inline function get_glowRadius() return this.uGlowRadius.value[0]; + inline function set_glowRadius(value:Float) + { + this.uGlowRadius.value = [Math.min(1, Math.max(0, value))]; + FlxG.watch.addQuick("glowRadius", value); + return value; + } + + @:glFragmentSource(' + #pragma header + + uniform vec2 uOrigin; + uniform float uScale; + uniform float uShade; + uniform float uGlowRadius; + + vec2 scalePos(vec2 p, float scale) + { + vec2 origin = uOrigin;// / openfl_TextureSize; + return origin + (p - origin) / scale; + } + + float getShadow(vec2 p) + { + // Not an effecient way to do this, but just scaling up the texture and put shadow if any part of it is "blocked" + float shadowAmount = 0.0; + for (float scale = 1.0; scale < 3.0; scale += 0.01) + { + shadowAmount = max(shadowAmount, texture2D(bitmap, scalePos(p, scale)).a); + } + return shadowAmount; + } + + float getGlow(vec2 p) + { + vec2 res = openfl_TextureSize; + p = p-uOrigin; + p.y *= res.y / res.x; + return 1.0 - smoothstep(uGlowRadius * 0.5, uGlowRadius, length(p)); + } + + const vec4 fgGlow = vec4(1.0, 0.125, 0.0, 0.5); + vec4 applyFgGlow(vec4 fg, float glowAmount) + { + vec3 glowRgb = fgGlow.rgb * fgGlow.a * glowAmount; + vec3 mult = fg.rgb * glowRgb; + vec3 add = fg.rgb + glowRgb; + return vec4((mult + add)/2.0, fg.a); + } + + const vec4 bgGlow = vec4(1.0, 0.125, 0.0, 0.25); + vec4 applyBgGlow(vec4 bg, float glowAmount) + { + vec3 glowRgb = bgGlow.rgb * bgGlow.a * glowAmount; + vec3 mult = bg.rgb * glowRgb; + vec3 add = bg.rgb + glowRgb; + return vec4((mult + add)/2.0, bg.a + (bgGlow.a * glowAmount)); + } + + void main() + { + vec2 uv = openfl_TextureCoordv; + + vec4 fg = texture2D(bitmap, uv); + vec4 shadow = vec4(0.0, 0.0, 0.02, uShade * getShadow(uv)); + float glowAmount = getGlow(uv); + + gl_FragColor = mix(applyBgGlow(shadow, glowAmount), applyFgGlow(fg, glowAmount), fg.a); + // gl_FragColor = glow; + } + ') + + public function new () + { + super(); + + shade = 0.6; + setOrigin(FlxG.width, FlxG.height); + glowRadius = 0.05; + } + + static var point = FlxPoint.get(); + public function setOrigin(x:Float, y:Float) + { + FlxG.watch.addQuick("origin", point.set(x, y)); + this.uOrigin.value = [x, y]; + } +} \ No newline at end of file