# cc/journeyman/the_great_game/world/heightmap.clj

001 (ns cc.journeyman.the-great-game.world.heightmap
002 "Functions dealing with the tessellated multi-layer heightmap."
003 (:require [clojure.math.numeric-tower :refer [expt sqrt]]
004 [mw-engine.core :refer []]
005 [mw-engine.heightmap :refer [apply-heightmap]]
006 [mw-engine.utils :refer [get-cell in-bounds? map-world]]
007 [cc.journeyman.the-great-game.utils :refer [value-or-default]]))
008
009 ;; It's not at all clear to me yet what the workflow for getting a MicroWorld
010 ;; map into The Great Game, and whether it passes through Walkmap to get here.
011 ;; This file as currently written assumes it doesn't.
012
013 ;; It's utterly impossible to hold a whole continent at one metre scale in
014 ;; memory at one time. So we have to be able to regenerate high resolution
015 ;; surfaces from much lower resolution heightmaps.
016 ;;
017 ;; Thus to reproduce a segment of surface at a particular level of detail,
018 ;; we:
019 ;; 1. load the base heightmap into a grid (see
020 ;; `mw-engine.heightmap/apply-heightmap`);
021 ;; 2. scale the base hightmap to kilometre scale (see `scale-grid`);
022 ;; 3. exerpt the portion of that that we want to reproduce (see `exerpt-grid`);
023 ;; 4. interpolate that grid to get the resolution we require (see
024 ;; `interpolate-grid`);
025 ;; 5. create an appropriate purturbation grid from the noise map(s) for the
026 ;; same coordinates to break up the smooth interpolation;
027 ;; 6. sum the altitudes of the two grids.
028 ;;
029 ;; In production this will have to be done **very** fast!
030
031 (def ^:dynamic *base-map* "resources/maps/heightmap.png")
032 (def ^:dynamic *noise-map* "resources/maps/noise.png")
033
034 (defn scale-grid
035 "multiply all `:x` and `:y` values in this `grid` by this `n`."
036 [grid n]
037 (map-world grid (fn [w c x] (assoc c :x (* (:x c) n) :y (* (:y c) n)))))
038
039
040
041 ;; Each of the east-west curve and the north-south curve are of course two
042 ;; dimensional curves; the east-west curve is in the :x/:z plane and the
043 ;; north-south curve is in the :y/:z plane (except, perhaps unwisely,
044 ;; we've been using :altitude to label the :z plane). We have a library
045 ;; function `walkmap.edge/intersection2d`, but as currently written it
046 ;; can only find intersections in :x/:y plane.
047 ;;
048 ;; TODO: rewrite the function so that it can use arbitrary coordinates.
049 ;; AFTER TRYING: OK, there are too many assumptions about the way that
050 ;; function is written to allow for easy rotation. TODO: think!
051
052 (defn interpolate-altitude
053 "Return the altitude of the point at `x-offset`, `y-offset` within this
054 `cell` having this `src-width`, taken from this `grid`."
055 [cell grid src-width x-offset y-offset ]
056 (let [c-alt (:altitude cell)
057 n-alt (or (:altitude (get-cell grid (:x cell) (dec (:y cell)))) c-alt)
058 w-alt (or (:altitude (get-cell grid (inc (:x cell)) (:y cell))) c-alt)
059 s-alt (or (:altitude (get-cell grid (:x cell) (inc (:y cell)))) c-alt)
060 e-alt (or (:altitude (get-cell grid (dec (:x cell)) (:y cell))) c-alt)]
061 ;; TODO: construct two curves (arcs of circles good enough for now)
062 ;; n-alt...c-alt...s-alt and e-alt...c-alt...w-alt;
063 ;; then interpolate x-offset along e-alt...c-alt...w-alt and y-offset
064 ;; along n-alt...c-alt...s-alt;
065 ;; then return the average of the two
066
067 0))
068
069 (defn interpolate-cell
070 "Construct a grid (array of arrays) of cells each of width `target-width`
071 from this `cell`, of width `src-width`, taken from this `grid`"
072 [cell grid src-width target-width]
073 (let [offsets (map #(* target-width %) (range (/ src-width target-width)))]
074 (into
075 []
076 (map
077 (fn [r]
078 (into
079 []
080 (map
081 (fn [c]
082 (assoc cell
083 :x (+ (:x cell) c)
084 :y (+ (:y cell) r)
085 :altitude (interpolate-altitude cell grid src-width c r)))
086 offsets)))
087 offsets))))
088
089 (defn interpolate-grid
090 "Return a grid interpolated from this `grid` of rows, cols given scaling
091 from this `src-width` to this `target-width`"
092 [grid src-width target-width]
093 (reduce
094 concat
095 (into
096 []
097 (map
098 (fn [row]
099 (reduce
100 (fn [g1 g2]
101 (into [] (map #(into [] (concat %1 %2)) g1 g2)))
102 (into [] (map #(interpolate-cell % grid src-width target-width) row))))
103 grid))))
104
105 (defn excerpt-grid
106 "Return that section of this `grid` where the `:x` co-ordinate of each cell
107 is greater than or equal to this `x-offset`, the `:y` co-ordinate is greater
108 than or equal to this `y-offset`, whose width is not greater than this
109 `width`, and whose height is not greater than this `height`."
110 [grid x-offset y-offset width height]
111 (into
112 []
113 (remove
114 nil?
115 (map
116 (fn [row]
117 (when
118 (and
119 (>= (:y (first row)) y-offset)
120 (< (:y (first row)) (+ y-offset height)))
121 (into
122 []
123 (remove
124 nil?
125 (map
126 (fn [cell]
127 (when
128 (and
129 (>= (:x cell) x-offset)
130 (< (:x cell) (+ x-offset width)))
131 cell))
132 row)))))
133 grid))))
134
135 (defn get-surface
136 "Return, as a vector of vectors of cells represented as Clojure maps, a
137 segment of surface from this `base-map` as modified by this
138 `noise-map` at this `cell-size` starting at this `x-offset` and `y-offset`
139 and having this `width` and `height`.
140
141 If `base-map` and `noise-map` are not supplied, the bindings of `*base-map*`
142 and `*noise-map*` will be used, respectively.
143
144 `base-map` and `noise-map` may be passed either as strings, assumed to be
145 file paths of PNG files, or as MicroWorld style world arrays. It is assumed
146 that one pixel in `base-map` represents one square kilometre in the game
147 world. It is assumed that `cell-size`, `x-offset`, `y-offset`, `width` and
148 `height` are integer numbers of metres."
149 ([cell-size x-offset y-offset width height]
150 (get-surface *base-map* *noise-map* cell-size x-offset y-offset width height))
151 ([base-map noise-map cell-size x-offset y-offset width height]
152 (let [b (if (seq? base-map) base-map (scale-grid (apply-heightmap base-map) 1000))
153 n (if (seq? noise-map) noise-map (apply-heightmap noise-map))]
154 (if (and (in-bounds? b x-offset y-offset)
155 (in-bounds? b (+ x-offset width) (+ y-offset height)))
156 b ;; actually do stuff
157 (throw (Exception. "Surface out of bounds for map.")))
158 )))
159