From db7bee7902f3e756d6df8fd902a011697c7443f3 Mon Sep 17 00:00:00 2001 From: AbnormalPoof Date: Thu, 8 Jan 2026 16:29:19 -0600 Subject: [PATCH] Add support for khronos blend modes, enabling 14 new blend modes to work properly Co-authored-by: Hyper_ <40342021+NotHyper-474@users.noreply.github.com> Co-authored-by: ACrazyTown <47027981+ACrazyTown@users.noreply.github.com> Co-authored-by: PurSnake <56314743+PurSnake@users.noreply.github.com> --- hmm.json | 4 +- project.hxp | 19 +- source/Main.hx | 4 + source/funkin/graphics/FunkinCamera.hx | 408 +++++++++--------- .../funkin/graphics/FunkinCameraFrontEnd.hx | 16 + .../graphics/framebuffer/BitmapDataUtil.hx | 167 ++++--- .../graphics/framebuffer/FixedBitmapData.hx | 46 +- .../shaders/RuntimeCustomBlendShader.hx | 8 + source/funkin/play/PlayState.hx | 22 +- source/funkin/play/stage/Stage.hx | 19 - .../ui/debug/stageeditor/StageEditorState.hx | 5 +- source/funkin/util/FileUtil.hx | 50 +-- 12 files changed, 401 insertions(+), 367 deletions(-) create mode 100644 source/funkin/graphics/FunkinCameraFrontEnd.hx diff --git a/hmm.json b/hmm.json index 5e23629e2..c8ad32c25 100644 --- a/hmm.json +++ b/hmm.json @@ -176,7 +176,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "318253ea3a28a3299a167748912c40444f403777", + "ref": "cae0c0bc10915f6281625d07e5527f3cecb4347f", "url": "https://github.com/FunkinCrew/lime" }, { @@ -190,7 +190,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "a69a6e778816c6a57eba7a91bfb0b60295508e88", + "ref": "caf5f604f733c4c19bfde42bda0380931a01f3c9", "url": "https://github.com/FunkinCrew/openfl" }, { diff --git a/project.hxp b/project.hxp index 1f348c985..0c93a393e 100644 --- a/project.hxp +++ b/project.hxp @@ -184,9 +184,7 @@ class Project extends HXProject /** * The songs whose audio files should be embedded inside the executable. */ - static final EMBED_SONGS_AUDIO:Array = [ - "spaghetti" - ]; + static final EMBED_SONGS_AUDIO:Array = ["spaghetti"]; // // BUILD FLAGS @@ -216,6 +214,13 @@ class Project extends HXProject */ static final EXCLUDE_ARMV7:FeatureFlag = "EXCLUDE_ARMV7"; + /** + * `-DFORCE_BLEND_SHADER` + * If this flag is enabled, the game will force the use of shaders to render + * certain blend modes, even if the device supports the needed OpenGL extensions. + */ + static final FORCE_BLEND_SHADER:FeatureFlag = "FORCE_BLEND_SHADER"; + /** * `-DGITHUB_BUILD` * If this flag is enabled, the game will use the configuration used by GitHub Actions @@ -855,6 +860,11 @@ class Project extends HXProject // Should be true on mobile builds. CENSOR_EXPLETIVES.apply(this, isMobile()); + // Should be true on Android. + // Some Android devices support native blends, but it causes issues + // with other shaders, so we'll disable it for now. + FORCE_BLEND_SHADER.apply(this, isAndroid()); + // // FEATURE FLAGS // @@ -1500,8 +1510,6 @@ class Project extends HXProject addAssetLibrary("sserafim", shouldEmbed, shouldPreload); addAssetPath("assets/sserafim", "assets/sserafim", "sserafim", ["*"], exclude, shouldEmbed, "assets"); - - // Art asset library (where README and CHANGELOG pull from) var shouldEmbedArt = false; var shouldPreloadArt = false; @@ -2166,7 +2174,6 @@ class Project extends HXProject { Sys.println("- " + libraryName.replace(",", ".") + " (Development Build)".blue().bold()); Sys.println((" Path: " + haxe.io.Path.removeTrailingSlashes(devPath)).blue()); - } else { diff --git a/source/Main.hx b/source/Main.hx index 1f4dd5372..8372e5059 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -173,6 +173,10 @@ class Main extends Sprite funkin.util.macro.ConsoleMacro.init(); #end + // Force a `FunkinCamera` to be the default camera. + // This allows the blend mode shader to work everywhere. + untyped FlxG.cameras = new funkin.graphics.FunkinCameraFrontEnd(); + #if !html5 FlxG.scaleMode = new FullScreenScaleMode(); #end diff --git a/source/funkin/graphics/FunkinCamera.hx b/source/funkin/graphics/FunkinCamera.hx index 4aa270d89..858e1f6b0 100644 --- a/source/funkin/graphics/FunkinCamera.hx +++ b/source/funkin/graphics/FunkinCamera.hx @@ -1,5 +1,6 @@ package funkin.graphics; +import animate.internal.RenderTexture; import flash.geom.ColorTransform; import flixel.FlxCamera; import flixel.graphics.FlxGraphic; @@ -7,22 +8,35 @@ import flixel.graphics.frames.FlxFrame; import flixel.math.FlxMatrix; import flixel.math.FlxRect; import flixel.system.FlxAssets.FlxShader; -import funkin.graphics.shaders.RuntimeCustomBlendShader; -import funkin.graphics.framebuffer.BitmapDataUtil; import funkin.graphics.framebuffer.FixedBitmapData; +import funkin.graphics.shaders.RuntimeCustomBlendShader; +import openfl.display.OpenGLRenderer; import openfl.Lib; import openfl.display.BitmapData; import openfl.display.BlendMode; -import openfl.display3D.textures.TextureBase; -import openfl.filters.BitmapFilter; -import openfl.filters.ShaderFilter; +import flixel.graphics.tile.FlxDrawQuadsItem; +import flixel.graphics.tile.FlxDrawTrianglesItem; + +using funkin.graphics.framebuffer.BitmapDataUtil; /** * A FlxCamera with additional powerful features: - * - Grab the camera screen as a `BitmapData` and use it as a texture - * - Support `sprite.blend = DARKEN/HARDLIGHT/LIGHTEN/OVERLAY` to apply visual effects using certain sprites - * - NOTE: Several other blend modes work without FunkinCamera. Some still do not work. - * - NOTE: Framerate-independent camera tweening is fixed in Flixel 6.x. Rest in peace, SwagCamera. + * - Added the ability to grab the camera screen as a `BitmapData` and use it as a texture. + * - Added support for the following blend modes for a sprite through shaders: + * - DARKEN + * - HARDLIGHT + * - LIGHTEN + * - OVERLAY + * - DIFFERENCE + * - INVERT + * - COLORDODGE + * - COLORBURN + * - SOFTLIGHT + * - EXCLUSION + * - HUE + * - SATURATION + * - COLOR + * - LUMINOSITY */ @:nullSafety @:access(openfl.display.DisplayObject) @@ -31,183 +45,141 @@ import openfl.filters.ShaderFilter; @:access(openfl.display3D.textures.TextureBase) @:access(flixel.graphics.FlxGraphic) @:access(flixel.graphics.frames.FlxFrame) +@:access(openfl.display.OpenGLRenderer) +@:access(openfl.geom.ColorTransform) class FunkinCamera extends FlxCamera { - final grabbed:Array = []; - final texturePool:Array = []; + /** + * Whether or not the device supports the OpenGL extension `KHR_blend_equation_advanced`. + * If `false`, a shader implementation will be used to render certain blend modes. + */ + public static var hasKhronosExtension(get, never):Bool; - final bgTexture:TextureBase; - final bgBitmap:BitmapData; - final bgFrame:FlxFrame; + static inline function get_hasKhronosExtension():Bool + { + #if FORCE_BLEND_SHADER + return false; + #else + @:privateAccess + return OpenGLRenderer.__complexBlendsSupported ?? false; + #end + } - final customBlendShader:RuntimeCustomBlendShader; - final customBlendFilter:ShaderFilter; + /** + * A list of blend modes that require the OpenGL extension `KHR_blend_equation_advanced`. + * + * NOTE: + * - `LIGHTEN` is supported natively on desktop, but not other platforms. + * - While `DARKEN` is supported natively on desktop, it causes issues with transparency. + */ + static final KHR_BLEND_MODES:Array = [ + DARKEN, + HARDLIGHT, + #if !desktop LIGHTEN, #end + OVERLAY, + DIFFERENCE, + COLORDODGE, + COLORBURN, + SOFTLIGHT, + EXCLUSION, + HUE, + SATURATION, + COLOR, + LUMINOSITY + ]; - var filtersApplied:Bool = false; - var bgItemCount:Int = 0; + /** + * A list of blend modes that require the shader no matter what. + * This is due to these blend modes not being supported on any platform. + */ + static final SHADER_REQUIRED_BLEND_MODES:Array = [INVERT]; - public var shouldDraw:Bool = true; + /** + * The ID of this camera, used for debugging. + */ + public var id:String; - // Used to identify the camera during debugging. - final id:String = 'unknown'; + var _blendShader:RuntimeCustomBlendShader; + var _backgroundFrame:FlxFrame; + + var _blendRenderTexture:RenderTexture; + var _backgroundRenderTexture:RenderTexture; + + var _cameraTexture:FixedBitmapData; + var _cameraMatrix:FlxMatrix; @:nullSafety(Off) public function new(id:String = 'unknown', x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0) { super(x, y, width, height, zoom); + this.id = id; - bgTexture = @:nullSafety(Off) pickTexture(width, height); - bgBitmap = FixedBitmapData.fromTexture(bgTexture); - bgFrame = new FlxFrame(new FlxGraphic('', null)); - bgFrame.parent.bitmap = bgBitmap; - bgFrame.frame = new FlxRect(); - customBlendShader = new RuntimeCustomBlendShader(); - customBlendFilter = new ShaderFilter(customBlendShader); - } - /** - * Grabs the camera screen and returns it as a `BitmapData`. The returned bitmap - * will not be referred by the camera so, changing it will not affect the scene. - * The returned bitmap **will be reused in the next frame**, so the content is available - * only in the current frame. - * @param applyFilters if this is `true`, the camera's filters will be applied to the grabbed bitmap, - * and the camera's filters will be disabled until the beginning of the next frame - * @param isolate if this is `true`, sprites to be rendered will only be rendered to the grabbed bitmap, - * and the grabbed bitmap will not include any previously rendered sprites - * @return the grabbed bitmap data - */ - public function grabScreen(applyFilters:Bool, isolate:Bool = false):Null - { - final texture = pickTexture(width, height); - final bitmap = FixedBitmapData.fromTexture(texture); - if (bitmap != null) - { - squashTo(bitmap, applyFilters, isolate); - grabbed.push(bitmap); - } - return bitmap; - } + _backgroundFrame = new FlxFrame(new FlxGraphic('', null)); + _backgroundFrame.frame = new FlxRect(); - /** - * Applies the filter immediately to the camera. This will be done independently from - * the camera's filters. This method can only be called after the first `grabScreen` - * in the frame. - * @param filter the filter - */ - public function applyFilter(filter:BitmapFilter):Void - { - if (grabbed.length == 0) - { - FlxG.log.error('grab screen before you can apply a filter!'); - return; - } - BitmapDataUtil.applyFilter(bgBitmap, filter); - } + _blendShader = new RuntimeCustomBlendShader(); - function squashTo(bitmap:BitmapData, applyFilters:Bool, isolate:Bool, clearScreen:Bool = false):Void - { - if (applyFilters && isolate) - { - FlxG.log.error('cannot apply filters while isolating!'); - } - if (filtersApplied && applyFilters) - { - FlxG.log.warn('filters already applied!'); - } - static final matrix = new FlxMatrix(); + _backgroundRenderTexture = new RenderTexture(this.width, this.height); + _blendRenderTexture = new RenderTexture(this.width, this.height); - // resize the background bitmap if needed - if (bgTexture.__width != width || bgTexture.__height != height) - { - BitmapDataUtil.resizeTexture(bgTexture, width, height); - bgBitmap.__resize(width, height); - bgFrame.parent.bitmap = bgBitmap; - } - - // grab the bitmap - renderSkipping(isolate ? bgItemCount : 0); - bitmap.fillRect(bitmap.rect, 0); - matrix.setTo(1, 0, 0, 1, flashSprite.x, flashSprite.y); - if (applyFilters) - { - bitmap.draw(flashSprite, matrix); - @:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed. - flashSprite.filters = null; - filtersApplied = true; - } - else - { - final tmp = flashSprite.filters; - @:nullSafety(Off) - flashSprite.filters = null; - bitmap.draw(flashSprite, matrix); - flashSprite.filters = tmp; - } - - if (!isolate) - { - // also copy to the background bitmap - bgBitmap.fillRect(bgBitmap.rect, 0); - bgBitmap.draw(bitmap); - } - - if (clearScreen) - { - // clear graphics data - super.clearDrawStack(); - canvas.graphics.clear(); - } - - // render the background bitmap - bgFrame.frame.set(0, 0, width, height); - matrix.setTo(viewWidth / width, 0, 0, viewHeight / height, viewMarginLeft, viewMarginTop); - drawPixels(bgFrame, matrix); - - // count background draw items for future isolation - bgItemCount = 0; - { - var item = _headOfDrawStack; - while (item != null) - { - item = item.next; - bgItemCount++; - } - } - } - - function renderSkipping(count:Int):Void - { - var item = _headOfDrawStack; - while (item != null) - { - if (--count < 0) item.render(this); - item = item.next; - } + _cameraMatrix = new FlxMatrix(); + _cameraTexture = FixedBitmapData.create(this.width, this.height); } override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader):Void { - if (!shouldDraw) return; + var shouldUseShader:Bool = (!hasKhronosExtension && KHR_BLEND_MODES.contains(blend)) || SHADER_REQUIRED_BLEND_MODES.contains(blend); - if ( switch blend - { - case DARKEN | HARDLIGHT | LIGHTEN | OVERLAY: true; - case _: false; - }) + // Fallback to the shader implementation if the device doesn't support `KHR_blend_equation_advanced`, or if + // the specified blend mode requires the shader. + if (shouldUseShader) { - // squash the screen - grabScreen(false); - // render without blend - super.drawPixels(frame, pixels, matrix, transform, null, smoothing, shader); - // get the isolated bitmap - final isolated = grabScreen(false, true); - // apply fullscreen blend - customBlendShader.blendSwag = blend; - @:nullSafety(Off) // I hope this doesn't cause issues - customBlendShader.sourceSwag = isolated; - customBlendShader.updateViewInfo(FlxG.width, FlxG.height, this); - applyFilter(customBlendFilter); + _cameraTexture.drawCameraScreen(this); + _backgroundFrame.frame.set(0, 0, this.width, this.height); + + // Clear the camera's graphics + // It'll get redrawn anyway + this.clearDrawStack(); + this.canvas.graphics.clear(); + + _blendRenderTexture.init(this.width, this.height); + _blendRenderTexture.drawToCamera((camera, frameMatrix) -> { + var pivotX:Float = width / 2; + var pivotY:Float = height / 2; + + frameMatrix.copyFrom(matrix); + frameMatrix.translate(-pivotX, -pivotY); + frameMatrix.scale(this.scaleX, this.scaleY); + frameMatrix.translate(pivotX, pivotY); + camera.drawPixels(frame, pixels, frameMatrix, transform, null, smoothing, shader); + }); + _blendRenderTexture.render(); + + _blendShader.sourceSwag = _blendRenderTexture.graphic.bitmap; + _blendShader.backgroundSwag = _cameraTexture; + + _blendShader.blendSwag = blend; + _blendShader.updateViewInfo(width, height, this); + + _backgroundFrame.parent.bitmap = _blendRenderTexture.graphic.bitmap; + + _backgroundRenderTexture.init(Std.int(this.width * Lib.current.stage.window.scale), Std.int(this.height * Lib.current.stage.window.scale)); + _backgroundRenderTexture.drawToCamera((camera, matrix) -> { + camera.zoom = this.zoom; + matrix.scale(Lib.current.stage.window.scale, Lib.current.stage.window.scale); + camera.drawPixels(_backgroundFrame, null, matrix, canvas.transform.colorTransform, null, false, _blendShader); + }); + + _backgroundRenderTexture.render(); + + // Resize the frame so it always fills the screen + _cameraMatrix.identity(); + _cameraMatrix.scale(1 / (this.scaleX * Lib.current.stage.window.scale), 1 / (this.scaleY * Lib.current.stage.window.scale)); + _cameraMatrix.translate(((width - width / this.scaleX) * 0.5), ((height - height / this.scaleY) * 0.5)); + + super.drawPixels(_backgroundRenderTexture.graphic.imageFrame.frame, null, _cameraMatrix, null, null, smoothing, null); } else { @@ -215,56 +187,76 @@ class FunkinCamera extends FlxCamera } } + override function startQuadBatch(graphic:FlxGraphic, colored:Bool, hasColorOffsets:Bool = false, ?blend:BlendMode, smooth:Bool = false, + ?shader:FlxShader):FlxDrawQuadsItem + { + // Can't batch complex non-coherent blends, so always force a new batch + if (hasKhronosExtension && !(OpenGLRenderer.__coherentBlendsSupported ?? false) && KHR_BLEND_MODES.contains(blend)) + { + var itemToReturn = null; + + if (FlxCamera._storageTilesHead != null) + { + itemToReturn = FlxCamera._storageTilesHead; + var newHead = FlxCamera._storageTilesHead.nextTyped; + itemToReturn.reset(); + FlxCamera._storageTilesHead = newHead; + } + else + { + itemToReturn = new FlxDrawQuadsItem(); + } + + // TODO: catch this error when the dev actually messes up, not in the draw phase + if (graphic.isDestroyed) throw 'Cannot queue ${graphic.key}. This sprite was destroyed.'; + + itemToReturn.graphics = graphic; + itemToReturn.antialiasing = smooth; + itemToReturn.colored = colored; + itemToReturn.hasColorOffsets = hasColorOffsets; + itemToReturn.blend = blend; + @:nullSafety(Off) + itemToReturn.shader = shader; + + itemToReturn.nextTyped = _headTiles; + _headTiles = itemToReturn; + + if (_headOfDrawStack == null) + { + _headOfDrawStack = itemToReturn; + } + + if (_currentDrawItem != null) + { + _currentDrawItem.next = itemToReturn; + } + + _currentDrawItem = itemToReturn; + + return itemToReturn; + } + + return super.startQuadBatch(graphic, colored, hasColorOffsets, blend, smooth, shader); + } + + override function startTrianglesBatch(graphic:FlxGraphic, smoothing:Bool = false, isColored:Bool = false, ?blend:BlendMode, ?hasColorOffsets:Bool, + ?shader:FlxShader):FlxDrawTrianglesItem + { + // Can't batch complex non-coherent blends, so always force a new batch + if (hasKhronosExtension + && !(OpenGLRenderer.__coherentBlendsSupported ?? false) + && KHR_BLEND_MODES.contains(blend)) return getNewDrawTrianglesItem(graphic, smoothing, isColored, blend, hasColorOffsets, shader); + + return super.startTrianglesBatch(graphic, smoothing, isColored, blend, hasColorOffsets, shader); + } + override function destroy():Void { super.destroy(); - disposeTextures(); - } - override function clearDrawStack():Void - { - super.clearDrawStack(); - // also clear grabbed bitmaps - for (bitmap in grabbed) - { - texturePool.push(bitmap.__texture); - bitmap.dispose(); // this doesn't release the texture - } - grabbed.clear(); - // clear filters applied flag - filtersApplied = false; - bgItemCount = 0; - } + _blendRenderTexture.destroy(); + _backgroundRenderTexture.destroy(); - function pickTexture(width:Int, height:Int):Null - { - // zero-sized textures will be problematic - width = width < 1 ? 1 : width; - height = height < 1 ? 1 : height; - if (texturePool.length > 0) - { - final res = texturePool.pop(); - if (res != null) BitmapDataUtil.resizeTexture(res, width, height); - else - trace('huh? why is this null? $texturePool'); - return res; - } - return Lib.current.stage.context3D.createTexture(width, height, BGRA, true); - } - - function disposeTextures():Void - { - for (bitmap in grabbed) - { - bitmap.dispose(); - } - grabbed.clear(); - for (texture in texturePool) - { - texture.dispose(); - } - texturePool.resize(0); - bgTexture.dispose(); - bgBitmap.dispose(); + _cameraTexture.dispose(); } } diff --git a/source/funkin/graphics/FunkinCameraFrontEnd.hx b/source/funkin/graphics/FunkinCameraFrontEnd.hx new file mode 100644 index 000000000..d3bb8d325 --- /dev/null +++ b/source/funkin/graphics/FunkinCameraFrontEnd.hx @@ -0,0 +1,16 @@ +package funkin.graphics; + +import flixel.FlxCamera; +import flixel.system.frontEnds.CameraFrontEnd; + +/** + * A `CameraFrontEnd` override that uses `FunkinCamera`! + */ +@:nullSafety +class FunkinCameraFrontEnd extends CameraFrontEnd +{ + public override function reset(?newCamera:FlxCamera):Void + { + super.reset(newCamera ?? new FunkinCamera()); + } +} diff --git a/source/funkin/graphics/framebuffer/BitmapDataUtil.hx b/source/funkin/graphics/framebuffer/BitmapDataUtil.hx index 234925d16..d83abf9f0 100644 --- a/source/funkin/graphics/framebuffer/BitmapDataUtil.hx +++ b/source/funkin/graphics/framebuffer/BitmapDataUtil.hx @@ -1,126 +1,151 @@ package funkin.graphics.framebuffer; -import flixel.FlxG; -import openfl.Lib; -import openfl.display.Bitmap; import openfl.display.BitmapData; -import openfl.display.Sprite; -import openfl.display3D.Context3DTextureFormat; +import openfl.display3D.Context3D; import openfl.display3D.textures.TextureBase; import openfl.filters.BitmapFilter; +import animate.internal.FilterRenderer; +import flixel.math.FlxMatrix; +import openfl.display.OpenGLRenderer; +import flixel.FlxCamera; +import openfl.Lib; +import openfl.geom.Matrix; +import openfl.geom.ColorTransform; /** - * Provides cool stuff for `BitmapData`s that have a hardware texture internally. + * A utility class for `BitmapData`s. */ @:nullSafety @:access(openfl.display.BitmapData) @:access(openfl.display3D.textures.TextureBase) @:access(openfl.display3D.Context3D) +@:access(openfl.display.OpenGLRenderer) +@:access(flixel.FlxCamera) +@:access(openfl.display.Sprite) +@:access(openfl.geom.ColorTransform) class BitmapDataUtil { - static function getCache():{sprite:Sprite, bitmap:Bitmap} + static var renderer(get, never):OpenGLRenderer; + static var _renderer:Null; + + static inline function get_renderer():OpenGLRenderer { - static var cache:Null<{sprite:Sprite, bitmap:Bitmap}> = null; - if (cache == null) + if (_renderer == null) { - final sprite = new Sprite(); - final bitmap = new Bitmap(); - sprite.addChild(bitmap); - cache = - { - sprite: sprite, - bitmap: bitmap - } + _renderer = new OpenGLRenderer(FlxG.stage.context3D); + _renderer.__worldTransform = new Matrix(); + _renderer.__worldColorTransform = new ColorTransform(); } - return cache; + + return _renderer; } /** - * Applies a bitmap filter to a bitmap immediately. The bitmap filter may refer - * the bitmap itself as a shader input. - * @param bitmap the bitmap data - * @param filter the bitmap filter + * Draws the contents of a camera onto a `BitmapData` object. + * + * Mostly copied from flixel-animate's `RenderTexture` + * Shoutouts to ACrazyTown and MaybeMaru this is some crazy work + * https://github.com/MaybeMaru/flixel-animate/blob/main/src/animate/internal/RenderTexture.hx + * + * @param bitmap The bitmap to draw onto. + * @param camera The camera to grab the screen from. + * + * @return The camera screen as a `BitmapData`. */ - public static function applyFilter(bitmap:BitmapData, filter:BitmapFilter):Void + public static function drawCameraScreen(bitmap:BitmapData, camera:FlxCamera):BitmapData { - hardwareCheck(bitmap); - final cache = getCache(); - cache.bitmap.bitmapData = bitmap; - cache.sprite.filters = [filter]; - bitmap.draw(cache.sprite); + var matrix:FlxMatrix = new FlxMatrix(); + var pivotX:Float = FlxG.scaleMode.scale.x; + var pivotY:Float = FlxG.scaleMode.scale.y; + + matrix.setTo(1 / pivotX, 0, 0, 1 / pivotY, camera.flashSprite.x / pivotX, camera.flashSprite.y / pivotY); + + bitmap.__fillRect(bitmap.rect, 0, true); + + camera.render(); + camera.flashSprite.__update(false, true); + + renderer.__cleanup(); + + renderer.setShader(renderer.__defaultShader); + renderer.__allowSmoothing = false; + renderer.__pixelRatio = Lib.current.stage.window.scale; + renderer.__worldAlpha = 1 / camera.flashSprite.__worldAlpha; + renderer.__worldTransform.copyFrom(camera.flashSprite.__renderTransform); + renderer.__worldTransform.invert(); + renderer.__worldTransform.concat(matrix); + renderer.__worldColorTransform.__copyFrom(camera.flashSprite.__worldColorTransform); + renderer.__worldColorTransform.__invert(); + renderer.__setRenderTarget(bitmap); + + bitmap.__drawGL(camera.canvas, renderer); + + return bitmap; } /** - * Creates a bitmap with a hardware texture. - * @param width the width - * @param height the height - * @param format the format if the internal texture - * @return the bitmap + * Applies a `BitmapFilter` to a bitmap. + * @param bitmap The `BitmapData` to apply the filter to. + * @param filter The filter to apply. + * + * @return The `BitmapData` with the filter applied. */ - public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):Null + public static function applyFilter(bitmap:BitmapData, filter:BitmapFilter):BitmapData { - final texture = Lib.current.stage.context3D.createTexture(width, height, format, true); - return FixedBitmapData.fromTexture(texture); + return FilterRenderer.applyFilter(null, bitmap, [filter]); } /** * Resizes the bitmap. - * @param bitmap the bitmap data - * @param width the width - * @param height the height + * @param bitmap The `BitmapData` to resize. + * @param width The new width. + * @param height The new height. */ public static function resize(bitmap:BitmapData, width:Int, height:Int):Void { - hardwareCheck(bitmap); if (bitmap.width == width && bitmap.height == height) return; + bitmap.width = width; bitmap.height = height; - resizeTexture(bitmap.__texture, width, height); + + if (!bitmap.readable) + { + resizeTexture(bitmap.__texture, width, height); + } } /** - * Resizes the texture. - * @param texture the texture - * @param width the width - * @param height the height + * Resizes the hardware texture. + * @param texture The `TextureBase` to resize. + * @param width The new width. + * @param height The new height. */ - @:nullSafety(Off) // the final context there is causing an error, idk how to fix it public static function resizeTexture(texture:TextureBase, width:Int, height:Int):Void { if (texture.__width == width && texture.__height == height) return; + + var context:Context3D = texture.__context; + texture.__width = width; texture.__height = height; - final context = texture.__context; - final gl = context.gl; + context.__bindGLTexture2D(texture.__textureID); - gl.texImage2D(gl.TEXTURE_2D, 0, texture.__internalFormat, width, height, 0, texture.__format, gl.UNSIGNED_BYTE, null); + context.gl.texImage2D(context.gl.TEXTURE_2D, 0, texture.__internalFormat, width, height, 0, texture.__format, context.gl.UNSIGNED_BYTE, null); + + @:nullSafety(Off) context.__bindGLTexture2D(null); } /** - * Copies the content of `src` to `dst`. The destination bitmap `dst` will be resized - * so that it has the same size as `src`. - * @param dst the destination bitmap - * @param src the source bitmap + * Copies the contents of `source` to `destination`. `destination` bitmap will be resized + * so that it has the same size as `source`. + * @param source The source `BitmapData`. + * @param destination The destination `BitmapData`. */ - @:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed. - public static function copy(dst:BitmapData, src:BitmapData):Void + public static function copy(source:BitmapData, destination:BitmapData):Void { - hardwareCheck(dst); - hardwareCheck(src); - final cache = getCache(); - cache.bitmap.bitmapData = src; - cache.sprite.filters = null; - resize(dst, src.width, src.height); - dst.fillRect(dst.rect, 0); - dst.draw(cache.sprite); - } - - static function hardwareCheck(bitmap:BitmapData):Void - { - if (bitmap.readable) - { - FlxG.log.error('do not use `BitmapDataUtil` for non-GPU bitmaps!'); - } + resize(destination, source.width, source.height); + destination.fillRect(destination.rect, 0); + destination.draw(source); } } diff --git a/source/funkin/graphics/framebuffer/FixedBitmapData.hx b/source/funkin/graphics/framebuffer/FixedBitmapData.hx index 21de9e50f..05de3d34a 100644 --- a/source/funkin/graphics/framebuffer/FixedBitmapData.hx +++ b/source/funkin/graphics/framebuffer/FixedBitmapData.hx @@ -6,6 +6,7 @@ import openfl.display.DisplayObjectContainer; import openfl.display.IBitmapDrawable; import openfl.display.OpenGLRenderer; import openfl.display3D.textures.TextureBase; +import openfl.Lib; /** * `BitmapData` is kinda broken so I fixed it. @@ -26,18 +27,47 @@ class FixedBitmapData extends BitmapData } /** - * Never use `BitmapData.fromTexture`, always use this. - * @param texture the texture - * @return the bitmap data + * Creates a `FixedBitmapData` with the given dimensions. + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param useGPU Whether or not this bitmap should use a hardware texture + * @return The newly created `FixedBitmapData` */ - public static function fromTexture(texture:Null):Null + public static function create(width:Int, height:Int, useGPU:Bool = true):FixedBitmapData { - if (texture == null) return null; - final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0); - // bitmapData.readable = false; + if (useGPU) + { + var texture:TextureBase = _createTexture(width, height); + return fromTexture(texture); + } + + return new FixedBitmapData(width, height, true, 0); + } + + /** + * Creates a `FixedBitmapData` from a hardware texture. + * @param texture The texture + * @return The newly created `FixedBitmapData` + */ + public static function fromTexture(texture:TextureBase):FixedBitmapData + { + var bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0); + bitmapData.readable = false; bitmapData.__texture = texture; bitmapData.__textureContext = texture.__textureContext; - // bitmapData.image = null; + + @:nullSafety(Off) + bitmapData.image = null; + return bitmapData; } + + static function _createTexture(width:Int, height:Int):TextureBase + { + // Zero-sized textures will be problematic. + width = width < 1 ? 1 : width; + height = height < 1 ? 1 : height; + + return Lib.current.stage.context3D.createTexture(width, height, BGRA, true); + } } diff --git a/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx b/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx index 74ce7f4ea..7b41d6fe0 100644 --- a/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx +++ b/source/funkin/graphics/shaders/RuntimeCustomBlendShader.hx @@ -14,6 +14,14 @@ class RuntimeCustomBlendShader extends RuntimePostEffectShader return sourceSwag = value; } + public var backgroundSwag(default, set):BitmapData; + + function set_backgroundSwag(value:BitmapData):BitmapData + { + this.setBitmapData("backgroundSwag", value); + return backgroundSwag = value; + } + // name change make sure it's not the same variable name as whatever is in the shader file public var blendSwag(default, set):BlendMode; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 69d8f6a92..0052ef715 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -591,12 +591,12 @@ class PlayState extends MusicBeatSubState /** * The camera which contains, and controls visibility of, the user interface elements. */ - public var camHUD:FlxCamera; + public var camHUD:FunkinCamera; /** * The camera which contains, and controls visibility of, the stage and characters. */ - public var camGame:FlxCamera; + public var camGame:FunkinCamera; /** * Simple helper debug variable, to be able to move the camera around for debug purposes @@ -607,22 +607,22 @@ class PlayState extends MusicBeatSubState /** * The camera which contains, and controls visibility of, a video cutscene, dialogue. */ - public var camCutscene:FlxCamera; + public var camCutscene:FunkinCamera; /** * The camera which contains, and controls visibility of menus when there are fake cutouts added. */ - public var camCutouts:FlxCamera; + public var camCutouts:FunkinCamera; /** * The camera which contains, and controls visibility of, the subtitles. */ - public var camSubtitles:FlxCamera; + public var camSubtitles:FunkinCamera; /** * The camera which contains, and controls visibility of, pause menu. */ - public var camPause:FlxCamera; + public var camPause:FunkinCamera; /** * The combo popups. Includes the real-time combo counter and the rating. @@ -788,11 +788,11 @@ class PlayState extends MusicBeatSubState // Cameras camGame = new FunkinCamera('playStateCamGame'); - camHUD = new FlxCamera(); - camCutscene = new FlxCamera(); - camCutouts = new FlxCamera(); - camSubtitles = new FlxCamera(); - camPause = new FlxCamera(); + camHUD = new FunkinCamera('playStateCamHUD'); + camCutscene = new FunkinCamera('playStateCamCutscene'); + camCutouts = new FunkinCamera('playStateCamCutouts'); + camSubtitles = new FunkinCamera('playStateCamSubtitles'); + camPause = new FunkinCamera('playStateCamPause'); var currentChart = currentSong.getDifficulty(currentDifficulty, currentVariation); var noteStyleId:Null = currentChart?.noteStyle; diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index e1ec2e093..ad8bc47e8 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -845,25 +845,6 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements */ function frameBuffersUpdated():Void {} - /** - * Grabs the current screen and returns it as a bitmap data. You can sefely modify it. - * @param applyFilters if this is `true`, the filters set to the camera will be applied to the resulting bitmap - * @return the grabbed screen - */ - function grabScreen(applyFilters:Bool):BitmapData - { - if (Std.isOfType(FlxG.camera, FunkinCamera)) - { - final cam:FunkinCamera = cast FlxG.camera; - return cam.grabScreen(applyFilters); - } - else - { - FlxG.log.error('cannot grab the screen: the main camera is not grabbable'); - return null; - } - } - public function onScriptEvent(event:ScriptEvent) { // Ensure all custom events get broadcast to the elements of the stage. diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index 71eff926f..2b6842c49 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -50,6 +50,7 @@ import haxe.ui.notifications.NotificationManager; import funkin.util.logging.CrashHandler; import funkin.graphics.shaders.Grayscale; import funkin.data.stage.StageRegistry; +import funkin.graphics.FunkinCamera; /** * Da Stage Editor woo!! @@ -252,7 +253,7 @@ class StageEditorState extends UIState var posCircles:Array = []; var camFields:FlxTypedGroup; var camHUD:FlxCamera; - var camGame:FlxCamera; + var camGame:FunkinCamera; public var camFollow:FlxObject; public var moveOffset:Array = []; @@ -305,7 +306,7 @@ class StageEditorState extends UIState AssetDataHandler.init(this); - camGame = new FlxCamera(); + camGame = new FunkinCamera(); camHUD = new FlxCamera(); camHUD.bgColor.alpha = 0; diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 3a441a61c..e851bec5a 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -1,20 +1,18 @@ package funkin.util; -import flixel.util.FlxTimer; -import haxe.io.Path; import haxe.zip.Entry; -import lime.ui.FileDialog; import lime.utils.Bytes; +import lime.ui.FileDialog; +import openfl.net.FileFilter; +import haxe.io.Path; +import openfl.net.FileReference; import openfl.events.Event; import openfl.events.IOErrorEvent; -import openfl.net.FileFilter; -import openfl.net.FileReference; #if FEATURE_HAXEUI -import haxe.ui.backend.OpenFileDialogBase.OpenFileDialogOptions; import haxe.ui.containers.dialogs.Dialog.DialogButton; import haxe.ui.containers.dialogs.Dialogs; -import haxe.ui.containers.dialogs.Dialogs.FileDialogExtensionInfo; import haxe.ui.containers.dialogs.Dialogs.SelectedFileInfo; +import haxe.ui.containers.dialogs.Dialogs.FileDialogExtensionInfo; #end using StringTools; @@ -136,7 +134,7 @@ class FileUtil } }; - openBrowseDialog(onComplete, + Dialogs.openFile(onComplete, { readContents: true, readAsBinary: true, // Binary @@ -169,7 +167,7 @@ class FileUtil } }; - openBrowseDialog(onComplete, + Dialogs.openFile(onComplete, { readContents: true, readAsBinary: false, // Text @@ -178,23 +176,6 @@ class FileUtil title: dialogTitle, }); } - - static function openBrowseDialog(onComplete:DialogButton->Array->Void, options:OpenFileDialogOptions):Void - { - // Resolve an issue where the game failed to handle focus loss during the browse dialog. - // We do this by forcing autoPause on, waiting a bit before opening the dialog, then restoring the value after the dialog closes. - - FlxG.autoPause = true; - - var callback:DialogButton->Array->Void = function(button, selectedFiles) { - FlxG.autoPause = Preferences.autoPause; - onComplete(button, selectedFiles); - }; - - FlxTimer.wait(0.2, function() { - Dialogs.openFile(callback, options); - }); - } #end /** @@ -1201,21 +1182,10 @@ class FileUtil #elseif linux var exitCode = Sys.command("xdg-open", [pathFolder]); if (exitCode == 0) return; - var fileManagers:Array = [ - "dolphin", - "nautilus", - "nemo", - "thunar", - "caja", - "konqueror", - "spacefm", - "pcmanfm" - ]; + var fileManagers:Array = ["dolphin", "nautilus", "nemo", "thunar", "caja", "konqueror", "spacefm", "pcmanfm"]; - for (fm in fileManagers) - { - if (Sys.command("which", [fm]) == 0) - { + for (fm in fileManagers) { + if (Sys.command("which", [fm]) == 0) { exitCode = Sys.command(fm, [pathFolder]); if (exitCode == 0) return; }