;;; Chickadee Game Toolkit ;;; Copyright © 2016, 2019, 2020 David Thompson ;;; ;;; Chickadee 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 3 of the License, ;;; or (at your option) any later version. ;;; ;;; Chickadee 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, see ;;; . (define-module (chickadee graphics sprite) #:use-module (rnrs bytevectors) #:use-module (srfi srfi-4) #:use-module (srfi srfi-9) #:use-module (srfi srfi-11) #:use-module (chickadee math matrix) #:use-module (chickadee math rect) #:use-module (chickadee math vector) #:use-module (chickadee graphics) #:use-module (chickadee graphics color) #:use-module (chickadee graphics shader) #:use-module (chickadee graphics texture) #:use-module (chickadee graphics buffer) #:export (draw-sprite* draw-sprite make-sprite-batch sprite-batch? sprite-batch-texture set-sprite-batch-texture! sprite-batch-clear! sprite-batch-add* sprite-batch-add! draw-sprite-batch* draw-sprite-batch with-batched-sprites draw-nine-patch* draw-nine-patch)) (define unbatched-sprite-shader (delay (strings->shader " #ifdef GLSL330 layout (location = 0) in vec2 position; layout (location = 1) in vec2 tex; #elif defined(GLSL130) in vec2 position; in vec2 tex; #elif defined(GLSL120) attribute vec2 position; attribute vec2 tex; #endif #ifdef GLSL120 varying vec2 fragTex; #else out vec2 fragTex; #endif uniform mat4 mvp; void main(void) { fragTex = tex; gl_Position = mvp * vec4(position.xy, 0.0, 1.0); } " " #ifdef GLSL120 varying vec2 fragTex; #else in vec2 fragTex; #endif #ifdef GLSL330 out vec4 fragColor; #endif uniform sampler2D colorTexture; uniform vec4 tint; void main (void) { #ifdef GLSL330 fragColor = texture(colorTexture, fragTex) * tint; #else gl_FragColor = texture2D(colorTexture, fragTex) * tint; #endif } "))) (define-geometry-type sprite-vertex-ref sprite-vertex-set! sprite-vertex-append! (position vec2) (texture vec2)) (define draw-sprite* (let* ((geometry (delay (make-geometry 4 #:index-capacity 6))) (mvp (make-null-matrix4))) (lambda* (texture rect matrix #:key (tint white) (blend-mode 'alpha) (texcoords (texture-gl-tex-rect texture))) (let ((geometry (force geometry))) (with-geometry geometry (let* ((x1 (rect-x rect)) (y1 (rect-y rect)) (x2 (+ x1 (rect-width rect))) (y2 (+ y1 (rect-height rect))) (s1 (rect-x texcoords)) (t1 (rect-y texcoords)) (s2 (+ (rect-x texcoords) (rect-width texcoords))) (t2 (+ (rect-y texcoords) (rect-height texcoords)))) ;; Texture origin is at the top-left, so we need to flip the Y ;; coordinate relative to the vertices. (sprite-vertex-append! geometry (x1 y1 s1 t2) (x2 y1 s2 t2) (x2 y2 s2 t1) (x1 y2 s1 t1)) (geometry-index-append! geometry 0 3 2 0 2 1))) (with-blend-mode blend-mode (with-texture 0 texture (gpu-apply (force unbatched-sprite-shader) (geometry-vertex-array geometry) #:tint tint #:mvp (if matrix (begin (matrix4-mult! mvp matrix (current-projection)) mvp) (current-projection))))))))) (define %null-vec2 (vec2 0.0 0.0)) (define %default-scale (vec2 1.0 1.0)) (define draw-sprite (let ((matrix (make-null-matrix4))) (lambda* (texture position #:key (tint white) (origin %null-vec2) (scale %default-scale) (rotation 0.0) (blend-mode 'alpha) (rect (texture-gl-rect texture))) "Draw TEXTURE at POSITION. Optionally, other transformations may be applied to the sprite. ROTATION specifies the angle to rotate the sprite, in radians. SCALE specifies the scaling factor as a 2D vector. All transformations are applied relative to ORIGIN, a 2D vector. TINT specifies the color to multiply against all the sprite's pixels. By default white is used, which does no tinting at all. By default, alpha blending is used but can be changed by specifying BLEND-MODE." (matrix4-2d-transform! matrix #:origin origin #:position position #:rotation rotation #:scale scale) (draw-sprite* texture rect matrix #:tint tint #:blend-mode blend-mode)))) ;;; ;;; Sprite Batches ;;; (define-geometry-type batched-sprite-ref batched-sprite-set! batched-sprite-append! (position vec2) (texture vec2) (tint vec4)) (define-record-type (%make-sprite-batch texture geometry size) sprite-batch? (texture sprite-batch-texture set-sprite-batch-texture!) (geometry sprite-batch-geometry) (size sprite-batch-size set-sprite-batch-size!)) (define* (make-sprite-batch texture #:key (capacity 256)) "Make a sprite batch that can hold CAPACITY sprites before needing to resize." (%make-sprite-batch texture (make-geometry (* capacity 4) #:index-capacity (* capacity 6)) 0)) (define (sprite-batch-clear! batch) "Reset BATCH to size 0." (set-sprite-batch-size! batch 0) (geometry-begin! (sprite-batch-geometry batch))) (define (sprite-batch-flush! batch) "Submit the contents of BATCH to the GPU." (geometry-end! (sprite-batch-geometry batch))) (define* (sprite-batch-add* batch rect matrix #:key (tint white) texture-region) "Add RECT, transformed by MATRIX, to BATCH. To render a subsection of the batch's texture, a texture object whose parent is the batch texture may be specified via the TEXTURE-REGION argument." (let* ((geometry (sprite-batch-geometry batch)) (vertex-offset (geometry-vertex-count geometry )) (minx (rect-x rect)) (miny (rect-y rect)) (maxx (+ minx (rect-width rect))) (maxy (+ miny (rect-height rect))) (x1 (matrix4-transform-x matrix minx miny)) (y1 (matrix4-transform-y matrix minx miny)) (x2 (matrix4-transform-x matrix maxx miny)) (y2 (matrix4-transform-y matrix maxx miny)) (x3 (matrix4-transform-x matrix maxx maxy)) (y3 (matrix4-transform-y matrix maxx maxy)) (x4 (matrix4-transform-x matrix minx maxy)) (y4 (matrix4-transform-y matrix minx maxy)) (texcoords (texture-gl-tex-rect (or texture-region (sprite-batch-texture batch)))) (s1 (rect-x texcoords)) (t1 (rect-y texcoords)) (s2 (+ (rect-x texcoords) (rect-width texcoords))) (t2 (+ (rect-y texcoords) (rect-height texcoords))) (r (color-r tint)) (g (color-g tint)) (b (color-b tint)) (a (color-a tint))) (batched-sprite-append! geometry (x1 y1 s1 t2 r g b a) (x2 y2 s2 t2 r g b a) (x3 y3 s2 t1 r g b a) (x4 y4 s1 t1 r g b a)) (geometry-index-append! geometry vertex-offset (+ vertex-offset 3) (+ vertex-offset 2) vertex-offset (+ vertex-offset 2) (+ vertex-offset 1)) (set-sprite-batch-size! batch (+ (sprite-batch-size batch) 1)))) (define sprite-batch-add! (let ((matrix (make-null-matrix4))) (lambda* (batch position #:key (origin %null-vec2) (scale %default-scale) (rotation 0.0) (tint white) texture-region) "Add sprite to BATCH at POSITION. To render a subsection of the batch's texture, a texture object whose parent is the batch texture may be specified via the TEXTURE-REGION argument." (let ((rect (texture-gl-rect (or texture-region (sprite-batch-texture batch))))) (matrix4-2d-transform! matrix #:origin origin #:position position #:rotation rotation #:scale scale) (sprite-batch-add* batch rect matrix #:tint tint #:texture-region texture-region))))) (define batched-sprite-shader (delay (strings->shader " #ifdef GLSL330 layout (location = 0) in vec2 position; layout (location = 1) in vec2 tex; layout (location = 2) in vec4 tint; #elif defined(GLSL130) in vec2 position; in vec2 tex; in vec4 tint; #elif defined(GLSL120) attribute vec2 position; attribute vec2 tex; attribute vec4 tint; #endif #ifdef GLSL120 varying vec2 fragTex; varying vec4 fragTint; #else out vec2 fragTex; out vec4 fragTint; #endif uniform mat4 mvp; void main(void) { fragTex = tex; fragTint = tint; gl_Position = mvp * vec4(position.xy, 0.0, 1.0); } " " #ifdef GLSL120 varying vec2 fragTex; varying vec4 fragTint; #else in vec2 fragTex; in vec4 fragTint; #endif #ifdef GLSL330 out vec4 fragColor; #endif uniform sampler2D colorTexture; void main (void) { #ifdef GLSL330 fragColor = texture(colorTexture, fragTex) * fragTint; #else gl_FragColor = texture2D(colorTexture, fragTex) * fragTint; #endif } "))) (define draw-sprite-batch* (let ((mvp (make-null-matrix4))) (lambda* (batch matrix #:key (blend-mode 'alpha)) "Render the contents of BATCH." (sprite-batch-flush! batch) (matrix4-mult! mvp matrix (current-projection)) (with-blend-mode blend-mode (with-texture 0 (sprite-batch-texture batch) (let ((geometry (sprite-batch-geometry batch))) (gpu-apply* (force batched-sprite-shader) (geometry-vertex-array geometry) 0 (geometry-index-count geometry) #:mvp mvp))))))) (define draw-sprite-batch (let ((matrix (make-null-matrix4))) (lambda* (batch #:key (position %null-vec2) (origin %null-vec2) (scale %default-scale) (rotation 0.0) (blend-mode 'alpha)) "Render the contents of BATCH." (matrix4-2d-transform! matrix #:origin origin #:position position #:rotation rotation #:scale scale) (draw-sprite-batch* batch matrix #:blend-mode blend-mode)))) ;;; ;;; Nine Patches ;;; (define draw-nine-patch* (let ((%rect (make-rect 0.0 0.0 0.0 0.0)) (texcoords (make-rect 0.0 0.0 0.0 0.0))) (lambda* (texture rect matrix #:key (margin 0.0) (top-margin margin) (bottom-margin margin) (left-margin margin) (right-margin margin) (blend-mode 'alpha) (tint white)) (let* ((x (rect-x rect)) (y (rect-y rect)) (w (rect-width rect)) (h (rect-height rect)) (border-x1 x) (border-y1 y) (border-x2 (+ x w)) (border-y2 (+ y h)) (fill-x1 (+ border-x1 left-margin)) (fill-y1 (+ border-y1 bottom-margin)) (fill-x2 (- border-x2 right-margin)) (fill-y2 (- border-y2 top-margin)) (prect (texture-gl-rect texture)) (trect (texture-gl-tex-rect texture)) (tw (rect-width prect)) (th (rect-height prect)) (border-s1 (rect-x trect)) (border-t1 (rect-y trect)) (border-s2 (+ (rect-x trect) (rect-width trect))) (border-t2 (+ (rect-y trect) (rect-height trect))) (fill-s1 (+ border-s1 (/ left-margin tw))) (fill-t1 (+ border-t1 (/ top-margin th))) (fill-s2 (- border-s2 (/ right-margin tw))) (fill-t2 (- border-t2 (/ bottom-margin th)))) (define (draw-piece x1 y1 x2 y2 s1 t1 s2 t2) (set-rect-x! %rect x1) (set-rect-y! %rect y1) (set-rect-width! %rect (- x2 x1)) (set-rect-height! %rect (- y2 y1)) (set-rect-x! texcoords s1) (set-rect-y! texcoords t1) (set-rect-width! texcoords (- s2 s1)) (set-rect-height! texcoords (- t2 t1)) (draw-sprite* texture %rect matrix #:texcoords texcoords #:blend-mode blend-mode #:tint tint)) ;; bottom-left (draw-piece border-x1 border-y1 fill-x1 fill-y1 border-s1 fill-t2 fill-s1 border-t2) ;; bottom-center (draw-piece fill-x1 border-y1 fill-x2 fill-y1 fill-s1 fill-t2 fill-s2 border-t2) ;; bottom-right (draw-piece fill-x2 border-y1 border-x2 fill-y1 fill-s2 fill-t2 border-s2 border-t2) ;; center-left (draw-piece border-x1 fill-y1 fill-x1 fill-y2 border-s1 fill-t2 fill-s1 fill-t1) ;; center (draw-piece fill-x1 fill-y1 fill-x2 fill-y2 fill-s1 fill-t2 fill-s2 fill-t1) ;; center-right (draw-piece fill-x2 fill-y1 border-x2 fill-y2 fill-s2 fill-t2 border-s2 fill-t1) ;; top-left (draw-piece border-x1 fill-y2 fill-x1 border-y2 border-s1 border-t1 fill-s1 fill-t1) ;; top-center (draw-piece fill-x1 fill-y2 fill-x2 border-y2 fill-s1 border-t1 fill-s2 fill-t1) ;; top-right (draw-piece fill-x2 fill-y2 border-x2 border-y2 fill-s2 border-t1 border-s2 fill-t1))))) (define draw-nine-patch (let ((position (vec2 0.0 0.0)) (%rect (make-rect 0.0 0.0 0.0 0.0)) (matrix (make-null-matrix4))) (lambda* (texture rect #:key (margin 0.0) (top-margin margin) (bottom-margin margin) (left-margin margin) (right-margin margin) (origin %null-vec2) (rotation 0.0) (scale %default-scale) (blend-mode 'alpha) (tint white)) "Draw a \"nine patch\" sprite. A nine patch sprite renders TEXTURE on the rectangular area RECT whose stretchable areas are defined by the given margin measurements. The corners are never stretched, the left and right edges may be stretched vertically, the top and bottom edges may be stretched horizontally, and the center may be stretched in both directions. This rendering technique is particularly well suited for resizable windows and buttons in graphical user interfaces. MARGIN specifies the margin size for all sides of the nine patch. To make margins of differing sizes, the TOP-MARGIN, BOTTOM-MARGIN, LEFT-MARGIN, and RIGHT-MARGIN arguments may be used." (set-rect-x! %rect 0.0) (set-rect-y! %rect 0.0) (set-rect-width! %rect (rect-width rect)) (set-rect-height! %rect (rect-height rect)) (set-vec2-x! position (rect-x rect)) (set-vec2-y! position (rect-y rect)) (matrix4-2d-transform! matrix #:origin origin #:position position #:rotation rotation #:scale scale) (draw-nine-patch* texture %rect matrix #:top-margin top-margin #:bottom-margin bottom-margin #:left-margin left-margin #:right-margin right-margin #:blend-mode blend-mode #:tint tint))))