1 /// 2 module isodi.raylib.display; 3 4 import std.conv; 5 import std.math; 6 import std.typecons; 7 import std.container; 8 import std.algorithm; 9 10 import raylib; 11 12 import isodi.bind; 13 import isodi.display; 14 import isodi.object3d; 15 import isodi.position; 16 import isodi.resource; 17 import isodi.raylib.cell; 18 import isodi.raylib.anchor; 19 import isodi.raylib.internal; 20 21 22 @safe: 23 24 25 /// 26 final class RaylibDisplay : Display { 27 28 /// Underlying raylib camera. 29 package raylib.Camera raycam; 30 31 /// 32 this() { 33 34 // Set camera constants 35 raycam.up = Vector3(0.0, 1.0, 0.0); 36 raycam.projection = CameraProjection.CAMERA_ORTHOGRAPHIC; 37 38 } 39 40 /// Get the underlying Raylib camera. 41 const(raylib.Camera) raylibCamera() const { 42 43 return raycam; 44 45 } 46 47 override void reloadResources() { 48 49 packs.clearCache(); 50 foreach (cell; cells) cell.reload(); 51 foreach (model; models) model.reload(); 52 53 } 54 55 /// Get Isodi position from world position. 56 /// Params: 57 /// original = Raylib world position. 58 Position isodiPosition(Vector3 original) const { 59 60 return position( 61 cast(int) floor(original.x / cellSize), 62 cast(int) floor(original.z / cellSize), 63 Height(original.y / cellSize), 64 ); 65 66 } 67 68 /// Get a ray from the mouse position relative to the camera. 69 /// Params: 70 /// inverted = Shoots the ray behind the camera. It might be useful to check both the normal and inverted ray 71 /// the ray doesn't recognize camera height properly. 72 Ray mouseRay(bool inverted) const @trusted { 73 74 auto ray = GetMouseRay(GetMousePosition, raycam); 75 76 if (inverted) { 77 78 ray.direction = Vector3Negate(ray.direction); 79 80 } 81 82 return ray; 83 84 } 85 86 /// Snap a Vector3 with a world position to Isodi. 87 Vector3 snapWorldPosition(Vector3 pos) const { 88 89 return Vector3( 90 cast(int) pos.x / cellSize * cellSize, 91 cast(int) pos.y / cellSize * cellSize, 92 cast(int) pos.z / cellSize * cellSize, 93 ); 94 95 } 96 97 /// Draw the contents of the display. 98 /// 99 /// Must be called inside `DrawingMode`, but not `BeginMode3D`. 100 void draw() @trusted { 101 102 updateCamera(); 103 104 // Draw 105 BeginMode3D(raycam); 106 scope (exit) EndMode3D(); 107 108 import std.array : array; 109 import std.range : chain; 110 111 rlOrtho(-1, 1, -1, 1, 0.01, cellSize * cellSize); 112 rlDisableDepthTest(); 113 114 alias PI = std.math.PI; 115 116 const rad = PI / 180; 117 const radX = camera.angle.x * rad; 118 const radY = camera.angle.y * rad; 119 120 // Get perceived screen size based on camera angle 121 // 90° = ×1, 0° = ×∞ 122 // No idea what would be the best formula here, at first I used tan, but it turned out to be excessive. 123 // This seems to work well... 124 const screenWidth = GetScreenWidth; 125 const screenHeight = cast(int) (GetScreenHeight * sqrt(1 + radY)); 126 127 // Get all 3D objects 128 chain( 129 cells.map!(a => cameraDistance(a, radX, 0)), 130 models.map!(a => cameraDistance(a, radX, 1)), 131 anchors.map!(a => cameraDistance(a, radX, 2, (cast(RaylibAnchor) a).drawOrder)) 132 ) 133 134 // Ignore invisible objects 135 .filter!(a => inBounds(a, screenWidth, screenHeight)) 136 137 // Depth sort 138 .array 139 .multiSort!(`a[1] < b[1]`, `a[2] > b[2]`, `a[3] < b[3]`, `a[4] < b[4]`) 140 141 // Draw them 142 .each!(a => a[0].to!WithDrawableResources.draw()); 143 144 } 145 146 private alias SortTuple = Tuple!(Object3D, RaylibAnchor.DrawOrder, float, float, uint); 147 148 /// Check if the object is in bounds of the display. 149 private bool inBounds(SortTuple object, int screenWidth, int screenHeight) { 150 151 Vector2 screenPoint(CellPoint point) @trusted { 152 153 return GetWorldToScreen(object[0].position.toVector3(cellSize, point), raycam); 154 155 } 156 157 const center = screenPoint(CellPoint.center); 158 const edge = screenPoint(CellPoint.edge); 159 160 const diagX = abs(edge.x - center.x); 161 const diagY = abs(edge.y - center.y); 162 163 // Top is visible 164 if (0 <= center.x + diagX && center.x - diagX < screenWidth 165 && 0 <= center.y + diagY && center.y - diagY < screenHeight) { 166 167 return true; 168 169 } 170 171 // Get bottom position 172 const bottom = screenPoint(CellPoint.bottomCenter); 173 174 // Bottom is visible 175 if (0 <= bottom.x + diagX && bottom.x - diagX < screenWidth 176 && 0 <= bottom.y + diagY && bottom.y - diagY < screenHeight) { 177 178 return true; 179 180 } 181 182 // Nope, nothing is visible 183 return false; 184 185 } 186 187 /// Get the camera distance of given Object3D 188 private SortTuple cameraDistance(Object3D object, real rad, uint priority, 189 RaylibAnchor.DrawOrder order = RaylibAnchor.DrawOrder.position) { 190 191 return SortTuple( 192 object, 193 order, 194 -object.visualPosition.x * sin(rad) 195 - object.visualPosition.y * cos(rad), 196 object.visualPosition.height.top, 197 priority, 198 ); 199 200 } 201 202 private void updateCamera() { 203 204 const rad = std.math.PI / 180; 205 const radX = camera.angle.x * rad; 206 const radY = camera.angle.y * rad; 207 const cosY = cos(camera.angle.y * rad); 208 209 // Calculate the target 210 const target = camera.follow is null 211 ? Position() 212 : camera.follow.visualPosition; 213 const targetVector = target.toVector3(cellSize, CellPoint.center); 214 215 // Update the camera 216 // not sure how to get the correct fovy from distance, this is just close to the expected result. 217 // TODO: figure it out. 218 raycam.fovy = camera.distance * cellSize; 219 220 // Get the target 221 raycam.target = Vector3( 222 targetVector.x + camera.offset.x * cellSize, 223 targetVector.y + camera.offset.height * cellSize, 224 targetVector.z + camera.offset.y * cellSize, 225 ); 226 227 // And place the camera 228 raycam.position = raycam.target + Vector3( 229 radX.sin * cosY, 230 radY.sin, 231 radX.cos * cosY, 232 ) * camera.distance; 233 234 } 235 236 /// Add a Raylib anchor. See `isodi.Anchor` for reference. 237 /// Params: 238 /// callback = Function that will be called every frame in order to draw the anchor content. 239 /// Returns: The created anchor 240 RaylibAnchor addAnchor(void delegate() @trusted callback) { 241 242 auto anchor = cast(RaylibAnchor) super.addAnchor(); 243 anchor.callback = callback; 244 return anchor; 245 246 } 247 248 }