commit b94e6113b078a14e579055074f8dbbcbbca0b9af Author: Tony Garnock-Jones Date: Tue Sep 8 20:11:16 2015 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..724bbe1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +compiled/ diff --git a/prospect-gl/2d.rkt b/prospect-gl/2d.rkt new file mode 100644 index 0000000..02707c2 --- /dev/null +++ b/prospect-gl/2d.rkt @@ -0,0 +1,336 @@ +#lang racket/gui + +(provide (struct-out window) + (struct-out frame-event) + (struct-out key-event) + (struct-out scene) + (except-out (struct-out sprite) sprite) + (rename-out [sprite ] [make-sprite sprite]) + update-scene + update-sprites + 2d-world) + +(require data/order) +(require data/splay-tree) +(require data/queue) +(require sgl/gl) +(require sgl/gl-vectors) + +(require (prefix-in image: 2htdp/image)) +(require (prefix-in pict: pict)) + +(require prospect) +(require prospect/route) +(require prospect/ground) + +(require "texture.rkt") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Shared state maintained by world. Describes current window dimensions. +(struct window (width height) #:transparent) + +;; Message sent by world. Describes frame about to be rendered. +(struct frame-event (counter timestamp) #:transparent) + +;; Message sent by world. Describes a key event. Key is a sealed +;; key-event%. +(struct key-event (code key) #:transparent) + +;; Shared state maintained by program. Prelude and postlude are to be +;; sealed instruction lists. It is an error to have more than exactly +;; one active such record at a given time. +(struct scene (prelude postlude) #:transparent) + +;; Shared state maintained by program. Z is to be a number, negative +;; toward camera. Instructions to be a sealed instruction list. +(struct sprite (z instructions) #:transparent) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (update-scene prelude postlude) + (patch-seq (retract (scene ? ?) #:meta-level 1) + (assert (scene (seal prelude) (seal postlude)) #:meta-level 1))) + +(define (make-sprite z instructions) + (sprite z (seal instructions))) + +(define (update-sprites . ss) + (patch-seq* (cons (retract (sprite ? ?) #:meta-level 1) + (map (lambda (s) (assert s #:meta-level 1)) ss)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(struct compiled-instructions (render-thunk resources)) + +(define-namespace-anchor ns-anchor) +(define ns (namespace-anchor->namespace ns-anchor)) + +(define (compile-instructions instrs) + (define-values (code resources) (instruction->racket-code `(begin ,@instrs))) + (define render-thunk (eval `(lambda () ,code) ns)) + (compiled-instructions render-thunk resources)) + +(define (compiled-instructions-dispose! i) + (when i + (for [(resource (compiled-instructions-resources i))] + (send resource dispose)))) + +(define (instructions->racket-code instrs) + (define-values (code-rev resources) + (for/fold [(code-rev '()) (resources '())] [(instr (in-list instrs))] + (define-values (new-code new-resources) (instruction->racket-code instr)) + (values (cons new-code code-rev) (append new-resources resources)))) + (values (reverse code-rev) resources)) + +(define (color-number? n) + (and (number? n) + (<= 0.0 n 1.0))) + +(define (instruction->racket-code instr) + (match instr + [`(rotate ,(? number? deg)) + (values `(glRotated ,deg) '())] + [`(scale ,(? number? x) ,(? number? y)) + (values `(glScaled ,x ,y 1) '())] + [`(translate ,(? number? x) ,(? number? y)) + (values `(glTranslated ,x ,y 0) '())] + [`(color ,(? color-number? r) ,(? color-number? g) ,(? color-number? b) ,(? color-number? a)) + (values `(glColor4d ,r ,g ,b ,a) '())] + [`(texture ,i) + (define tex (image->texture i)) + (values `(draw-gl-face ,tex) (list tex))] + [`(push-matrix ,instr ...) + (define-values (code resources) (instructions->racket-code instr)) + (values `(begin (glPushMatrix) ,@code (glPopMatrix)) resources)] + [`(begin ,instr ...) + (define-values (code resources) (instructions->racket-code instr)) + (values `(begin ,@code (void)) resources)] + [other + (error 'instruction->racket-code "unknown render instruction: ~v" other)])) + +(define (image->bitmap i) + (cond + [(image:image? i) + (define w (image:image-width i)) + (define h (image:image-height i)) + (define bm (make-object bitmap% w h #f #t)) + (define dc (send bm make-dc)) + (send i draw dc + 0 0 + 0 0 + w h + 0 0 + #f) + bm] + [(pict:pict? i) + (pict:pict->bitmap i)] + [(is-a? i bitmap%) + i] + [else + (error 'image->bitmap "unsupported image type ~v" i)])) + +(define (image->texture i) + (define bm (image->bitmap i)) + (new texture% [bitmap bm])) + +;; (define (lerp a b v) (+ (* v a) (* (- 1 v) b))) + +(define (draw-gl-face texture) + (send texture bind-texture) + (glBegin GL_QUADS) + (glNormal3d 0 0 -1) + (glTexCoord2i 0 0) + (glVertex3d 0 0 0) + (glTexCoord2i 1 0) + (glVertex3d 1 0 0) + (glTexCoord2i 1 1) + (glVertex3d 1 1 0) + (glTexCoord2i 0 1) + (glVertex3d 0 1 0) + (glEnd)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define empty-instructions (compile-instructions '())) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define scene-projection (compile-projection (?! (scene ? ?)))) +(define sprite-projection (compile-projection (?! (sprite ? ?)))) + +(define sprite-order + (order 'sprite-order + sprite? + (lambda (a b) (and (= (sprite-z a) (sprite-z b)) + (eq? (sprite-instructions a) + (sprite-instructions b)))) + (lambda (a b) (or (> (sprite-z a) (sprite-z b)) + (and (= (sprite-z a) (sprite-z b)) + (< (eq-hash-code (sprite-instructions a)) + (eq-hash-code (sprite-instructions b)))))))) + +(define (remove-sprite! sprites s) + (compiled-instructions-dispose! (splay-tree-ref sprites s #f)) + (splay-tree-remove! sprites s)) + +(define (add-sprite! sprites s) + (define instrs `((color 1 1 1 1) + (push-matrix ,@(seal-contents (sprite-instructions s))))) + (define i (compile-instructions instrs)) + (splay-tree-set! sprites s i)) + +(define (render-scene! prelude sprites postlude) + ((compiled-instructions-render-thunk prelude)) + (let loop ((iter (splay-tree-iterate-first sprites))) + (when iter + ((compiled-instructions-render-thunk (splay-tree-iterate-value sprites iter))) + (loop (splay-tree-iterate-next sprites iter)))) + ((compiled-instructions-render-thunk postlude))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define world-canvas% + (class canvas% + (inherit refresh with-gl-context swap-gl-buffers) + + (init boot-actions) + + (define counter 0) + (define start-time (current-inexact-milliseconds)) + (define/public (sim-time) + (- (current-inexact-milliseconds) start-time)) + + (define initialised? #f) + + (define near-depth 10) ;; 2.5D + (define far-depth 15) ;; 2.5D + + (define prelude empty-instructions) + (define sprites (make-splay-tree sprite-order)) + (define postlude empty-instructions) + (define fullscreen? #f) + + (define world (make-world boot-actions)) + (define event-queue (make-queue)) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (define (inject-event! e) + (enqueue! event-queue e)) + + (define (deliver-event e) + (clean-transition (world-handle-event e world))) + + (define (quiesce!) + (let loop ((txn #f) (need-poll? #t)) + (match txn + [#f ;; inert + (if (queue-empty? event-queue) + (when need-poll? (loop (deliver-event #f) #f)) + (loop (deliver-event (dequeue! event-queue)) #t))] + [(transition new-world actions) + (set! world new-world) + (for-each process-action! actions) + (loop #f #t)]))) + + (define (process-action! a) + (match a + [(? patch? p) + (process-scene-updates! p) + (process-sprite-updates! p) + (process-stop-requests! p) + (process-fullscreen-requests! p)] + [(message body) (void)])) + + (define (process-scene-updates! p) + (define-values (added removed) (patch-project/set/single p scene-projection)) + (when (not (set-empty? removed)) + (set! prelude empty-instructions) + (set! postlude empty-instructions)) + (for [(s added)] + (match-define (scene (seal pre) (seal post)) s) + (set! prelude (compile-instructions pre)) + (set! post (compile-instructions post)))) + + (define (process-sprite-updates! p) + (define-values (added removed) (patch-project/set/single p sprite-projection)) + ;; Remove old sprites first, to recycle their texture identifiers (if any) + (for [(s removed)] (remove-sprite! sprites s)) + (for [(s added)] (add-sprite! sprites s))) + + (define (process-stop-requests! p) + (when (matcher-match-value (patch-added p) 'stop #f) + (send (send this get-top-level-window) show #f))) + + (define (process-fullscreen-requests! p) + (define changed? #f) + (when (matcher-match-value (patch-removed p) 'fullscreen #f) + (set! changed? #t) + (set! fullscreen? #f)) + (when (matcher-match-value (patch-added p) 'fullscreen #f) + (set! changed? #t) + (set! fullscreen? #t)) + (when changed? + (send (send this get-top-level-window) fullscreen fullscreen?))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (define/override (on-paint) + (with-gl-context + (lambda () + (inject-event! (message (frame-event counter (sim-time)))) + (set! counter (+ counter 1)) + (quiesce!) + (unless initialised? + (glBlendFunc GL_ONE GL_ONE_MINUS_SRC_ALPHA) ;; premultiplied + (glEnable GL_BLEND) + (glEnable GL_TEXTURE_2D) + (glClearColor 0 0 0 1) + (set! initialised? #t)) + (glClear GL_COLOR_BUFFER_BIT) + (glLoadIdentity) + (glTranslated 0 0 (- near-depth)) + (render-scene! prelude sprites postlude) + (glFlush) + (swap-gl-buffers))) + (queue-callback (lambda () (refresh)) #f)) + + (define/override (on-size width height) + (with-gl-context + (lambda () + (inject-event! (patch-seq (retract (window ? ?)) + (assert (window width height)))) + (quiesce!) + (glViewport 0 0 width height) + (glMatrixMode GL_PROJECTION) + (glLoadIdentity) + (glOrtho 0 width height 0 0.1 100) + (glMatrixMode GL_MODELVIEW) + (glLoadIdentity))) + (refresh)) + + (define/override (on-char key) + (with-gl-context + (lambda () + (inject-event! (message (key-event (send key get-key-code) (seal key)))) + (quiesce!)))) + + (super-new (style '(gl no-autoclear))))) + +(define (2d-world #:width [width #f] + #:height [height #f] + . boot-actions) + (define frame (new frame% + [style '(fullscreen-button)] + [label "prospect-gl"] + [width (or width 640)] + [height (or height 480)])) + (define c (new world-canvas% + [parent frame] + [boot-actions boot-actions])) + (unless (send (send (send c get-dc) get-gl-context) ok?) + (error '2d-world "OpenGL context failed to initialize")) + (send c focus) + (send frame show #t) + (yield 'wait)) diff --git a/prospect-gl/examples/basic.rkt b/prospect-gl/examples/basic.rkt new file mode 100644 index 0000000..dcbf819 --- /dev/null +++ b/prospect-gl/examples/basic.rkt @@ -0,0 +1,41 @@ +#lang racket + +(require prospect) +(require 2htdp/image) +(require "../2d.rkt") + +(define window-projection (compile-projection (at-meta (?! (window ? ?))))) + +(2d-world (spawn (lambda (e s) + (match e + [(? patch? p) + (define-values (added removed) (patch-project/set/single p window-projection)) + (transition s (for/list [(w added)] + (match-define (window width height) w) + (update-scene `((push-matrix (scale ,width ,height) + (texture ,(rectangle 1 1 "solid" "white")))) + `())))] + [_ #f])) + (void) + (sub (window ? ?) #:meta-level 1) + ;; (assert 'fullscreen #:meta-level 1) + ) + (spawn (lambda (e s) #f) + (void) + (update-sprites (sprite 0 `((translate 50 50) + (scale 50 50) + (texture ,(circle 50 "solid" "orange")) + )) + (sprite -1 `((translate 60 60) + (scale 50 50) + (texture ,(circle 50 "solid" "green")) + )))) + (spawn (lambda (e s) + (match e + [(message _) + (transition s (assert 'stop #:meta-level 1))] + [_ #f])) + (void) + (sub (key-event #\q ?) #:meta-level 1)) + ) +(exit 0) diff --git a/prospect-gl/texture.rkt b/prospect-gl/texture.rkt new file mode 100644 index 0000000..d733332 --- /dev/null +++ b/prospect-gl/texture.rkt @@ -0,0 +1,49 @@ +#lang racket/gui + +(provide texture%) + +(require sgl/gl) +(require sgl/gl-vectors) + +(define texture% + (class object% + (init [(initial-bitmap bitmap)]) + (field [width 0] + [height 0] + [textures #f]) + + (define/public (get-width) width) + (define/public (get-height) height) + + (define/public (bind-texture) + (when (not textures) (error 'bind-texture "Attempt to use disposed texture%")) + (glBindTexture GL_TEXTURE_2D (gl-vector-ref textures 0))) + + (define/public (load-from-bitmap! bitmap) + (when textures (dispose)) + (set! textures (glGenTextures 1)) + (bind-texture) + (define image-data + (let () + (set! width (send bitmap get-width)) + (set! height (send bitmap get-height)) + (define dc (new bitmap-dc% [bitmap bitmap])) + (define pixels (* width height)) + (define vec (make-gl-ubyte-vector (* pixels 4))) + (define data (make-bytes (* pixels 4))) + (send dc get-argb-pixels 0 0 width height data #f #t) ;; premultiplied + (for ((i (in-range pixels))) + (for ((j (in-range 4))) + (gl-vector-set! vec (+ (* i 4) j) (bytes-ref data (+ (* i 4) (- 3 j)))))) + vec)) + (glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MIN_FILTER GL_LINEAR) + (glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MAG_FILTER GL_LINEAR) + (glTexImage2D GL_TEXTURE_2D 0 4 width height 0 GL_BGRA GL_UNSIGNED_BYTE image-data)) + + (define/public (dispose) + (when textures + (glDeleteTextures textures) + (set! textures #f))) + + (super-new) + (load-from-bitmap! initial-bitmap)))