1 /** 2 * Retrograde Engine 3 * 4 * Authors: 5 * Mike Bierlee, m.bierlee@lostmoment.com 6 * Copyright: 2014-2021 Mike Bierlee 7 * License: 8 * This software is licensed under the terms of the MIT license. 9 * The full terms of the license can be found in the LICENSE.txt file. 10 */ 11 12 module retrograde.graphics.core; 13 14 import retrograde.entity; 15 import retrograde.math; 16 import retrograde.file; 17 import retrograde.game; 18 import retrograde.stringid; 19 import retrograde.cache; 20 21 import poodinis; 22 23 import std.string; 24 import std.math; 25 import std.exception; 26 import std.conv; 27 28 enum TextureFilterMode { 29 nearestNeighbor, 30 linear, 31 trilinear 32 } 33 34 abstract class Texture { 35 public abstract RectangleU getTextureSize(); 36 public abstract string getName(); 37 } 38 39 class TextureComponent : EntityComponent, Snapshotable { 40 mixin EntityComponentIdentity!"TextureComponent"; 41 42 public Texture texture; 43 44 this(Texture texture) { 45 this.texture = texture; 46 } 47 48 public string[string] getSnapshotData() { 49 auto textureName = texture !is null ? texture.getName : ""; 50 return ["textureName": textureName]; 51 } 52 } 53 54 class RenderableSpriteComponent : EntityComponent { 55 mixin EntityComponentIdentity!"RenderableSpriteComponent"; 56 } 57 58 class RenderableTextComponent : EntityComponent { 59 mixin EntityComponentIdentity!"RenderableTextComponent"; 60 } 61 62 class HideableComponent : EntityComponent, Snapshotable { 63 mixin EntityComponentIdentity!"HideableComponent"; 64 65 public bool isHidden; 66 67 this() { 68 this(false); 69 } 70 71 this(bool isHidden) { 72 this.isHidden = isHidden; 73 } 74 75 public string[string] getSnapshotData() { 76 return ["isHidden": isHidden ? "true": "false"]; 77 } 78 } 79 80 class SpriteAnimationComponent : EntityComponent, Snapshotable { 81 mixin EntityComponentIdentity!"SpriteAnimationComponent"; 82 83 public long msecsInProgress; 84 85 private ulong _currentFrame; 86 private ulong _msecsPerFrame; 87 private SpritesheetAnimation _spritesheetAnimation; 88 private Animation _currentAnimation; 89 90 public @property SpritesheetAnimation spritesheetAnimation() { 91 return _spritesheetAnimation; 92 } 93 94 public @property Animation currentAnimation() { 95 return _currentAnimation; 96 } 97 98 public @property void currentAnimation(Animation animation) { 99 _currentAnimation = animation; 100 currentFrame = animation.beginFrame; 101 msecsInProgress = 0; 102 } 103 104 public @property long msecsPerFrame() { 105 return this._msecsPerFrame; 106 } 107 108 public @property void currentFrame(ulong currentFrame) { 109 enforce(currentFrame >= _currentAnimation.beginFrame && currentFrame <= _currentAnimation.endFrame, 110 format("Set frame is not within the range of the start and end frame. Frame: %s, Start: %s, End: %s", 111 currentFrame, _currentAnimation.beginFrame, _currentAnimation.endFrame)); 112 this._currentFrame = currentFrame; 113 } 114 115 public @property ulong currentFrame() { 116 return this._currentFrame; 117 } 118 119 private long getMsecsPerFrame(ulong framesPerSecond) { 120 return framesPerSecond == 0 ? 0 : cast(long) round(1000 / framesPerSecond); 121 } 122 123 public this(SpritesheetAnimation spritesheetAnimation) { 124 _spritesheetAnimation = spritesheetAnimation; 125 currentAnimation = spritesheetAnimation.initialAnimation; 126 _msecsPerFrame = getMsecsPerFrame(spritesheetAnimation.framesPerSecond); 127 } 128 129 public void setAnimation(string animationName) { 130 currentAnimation = _spritesheetAnimation.animations[animationName]; 131 } 132 133 public string[string] getSnapshotData() { 134 return [ 135 "currentFrame": to!string(_currentFrame), 136 "msecsInProgress": to!string(msecsInProgress), 137 "msecsPerFrame": to!string(_msecsPerFrame), 138 "currentAnimation": to!string(_currentAnimation.name) 139 ]; 140 } 141 } 142 143 class SpriteSheetComponent : EntityComponent, Snapshotable { 144 mixin EntityComponentIdentity!"SpriteSheetComponent"; 145 146 public RectangleUL size; 147 public Spritesheet spritesheet; 148 149 public this(Spritesheet spritesheet, RectangleUL size) { 150 this.size = size; 151 this.spritesheet = spritesheet; 152 } 153 154 public RectangleUL getSprite(ulong row, ulong column) { 155 enforce(row <= spritesheet.rows, 156 format("Row %s illegal, sprite has %s rows", row, spritesheet.rows)); 157 enforce(column <= spritesheet.columns, 158 format("Column %s illegal, sprite has %s columns", column, spritesheet.columns)); 159 enforce(row != 0, "Invalid row number 0, rows are 1-indexed"); 160 enforce(column != 0, "Invalid column number 0, columns are 1-indexed"); 161 162 auto spriteWidth = size.width / spritesheet.columns; 163 auto spriteHeight = size.height / spritesheet.rows; 164 auto spriteX = spriteWidth * (column - 1); 165 auto spriteY = spriteHeight * (row - 1); 166 167 return RectangleUL(spriteX, spriteY, spriteWidth, spriteHeight); 168 } 169 170 public RectangleUL getNthSprite(ulong n) { 171 enforce(n <= spritesheet.spriteCount, 172 format("Sprite number %s invalid, the spritesheet only has %s sprites", 173 n, spritesheet.spriteCount)); 174 enforce(n != 0, "Invalid sprite number 0, sprite count is 1-indexed"); 175 176 ulong row = cast(uint) ceil(cast(double) n / spritesheet.columns); 177 ulong column = n - ((row - 1) * spritesheet.columns); 178 179 return getSprite(row, column); 180 } 181 182 public string[string] getSnapshotData() { 183 return ["filename": spritesheet.fileName]; 184 } 185 } 186 187 interface TextureComponentFactory { 188 TextureComponent loadTexture(File textureFile); 189 TextureComponent createNullComponent(); 190 } 191 192 class RenderableSpriteEntityCreationParameters : CreationParameters { 193 public File textureFile; 194 public Vector2D position; 195 public scalar orientation; 196 197 this(File textureFile, Vector2D position = Vector2D(0), scalar orientation = 0) { 198 this.textureFile = textureFile; 199 this.position = position; 200 this.orientation = orientation; 201 } 202 } 203 204 class RenderableSpriteEntityFactory : EntityFactory { 205 @Autowire private TextureComponentFactory textureComponentFactory; 206 207 this() { 208 super("ent_sprite"); 209 } 210 211 public override Entity createEntity(CreationParameters parameters) { 212 auto textureCreationParameters = cast(RenderableSpriteEntityCreationParameters) parameters; 213 auto entity = createBlankEntity(); 214 215 auto textureComponent = textureComponentFactory.loadTexture( 216 textureCreationParameters.textureFile); 217 entity.addComponent(textureComponent); 218 entity.addComponent(new Position2DComponent(textureCreationParameters.position)); 219 entity.addComponent(new OrientationR2Component(textureCreationParameters.orientation)); 220 entity.addComponent!RenderableSpriteComponent; 221 222 return entity; 223 } 224 225 public Entity createEntity(File textureFile, Vector2D position = Vector2D(0), 226 scalar orientation = 0) { 227 auto parameters = new RenderableSpriteEntityCreationParameters(textureFile, 228 position, orientation); 229 return createEntity(parameters); 230 } 231 } 232 233 alias TextureCache = Cache!(string, Texture); 234 235 class SpriteAnimationProcessor : EntityProcessor { 236 237 @Autowire private Game game; 238 239 public override bool acceptsEntity(Entity entity) { 240 return entity.hasComponent!SpriteAnimationComponent; 241 } 242 243 public override void update() { 244 foreach (entity; entities) { 245 animateEntitySprite(entity); 246 } 247 } 248 249 private void animateEntitySprite(Entity entity) { 250 enforce(game.targetFrameTime > 0, "SpriteAnimationProcessor cannot operate with a framerate of 0. Remove the SpriteAnimationComponent from the entity or specify a non-zero framerate in your game."); 251 252 entity.withComponent!SpriteAnimationComponent((c) { 253 if (c.msecsPerFrame == 0) { 254 return; 255 } 256 257 c.msecsInProgress += game.targetFrameTime; 258 if (c.msecsInProgress >= c.msecsPerFrame) { 259 auto frames = floor(cast(double) c.msecsInProgress / c.msecsPerFrame); 260 c.msecsInProgress %= c.msecsPerFrame; 261 262 while (frames--) { 263 if (c.currentFrame == c.currentAnimation.endFrame) { 264 c.currentFrame = c.currentAnimation.beginFrame; 265 } else { 266 c.currentFrame = c.currentFrame + 1; 267 } 268 } 269 } 270 }); 271 } 272 273 public override void draw() { 274 } 275 } 276 277 class HorizontalSpriteFlipComponent : EntityComponent { 278 mixin EntityComponentIdentity!"HorizontalSpriteFlipComponent"; 279 } 280 281 class VerticalSpriteFlipComponent : EntityComponent { 282 mixin EntityComponentIdentity!"VerticalSpriteFlipComponent"; 283 } 284 285 class SpritesheetAnimation { 286 public ulong framesPerSecond; 287 public Animation initialAnimation; 288 public Spritesheet[ulong] spritesheets; 289 public Animation[string] animations; 290 } 291 292 class Spritesheet { 293 public ulong id; 294 public ulong columns; 295 public ulong rows; 296 public string fileName; 297 298 this(ulong rows = 1, ulong columns = 1, string fileName = "", ulong id = 0) { 299 this.rows = rows; 300 this.columns = columns; 301 this.fileName = fileName; 302 this.id = id; 303 } 304 305 public @property ulong spriteCount() { 306 return columns * rows; 307 } 308 } 309 310 class Animation { 311 public string name; 312 public ulong beginFrame; 313 public ulong endFrame; 314 public Spritesheet spritesheet; 315 316 this(ulong beginFrame = 1, ulong endFrame = 1, string name = "", Spritesheet spritesheet = null) { 317 this.beginFrame = beginFrame; 318 this.endFrame = endFrame; 319 this.name = name; 320 this.spritesheet = spritesheet; 321 } 322 } 323 324 class RenderOrderComponent : EntityComponent, Snapshotable { 325 mixin EntityComponentIdentity!"RenderOrderComponent"; 326 327 private int _order; 328 329 public @property int order() { 330 return _order; 331 } 332 333 this(int order = 0) { 334 this._order = order; 335 } 336 337 public string[string] getSnapshotData() { 338 return ["order": to!string(_order)]; 339 } 340 } 341 342 class DrawingOffset2DComponent : EntityComponent, Snapshotable { 343 mixin EntityComponentIdentity!"DrawingOffset2DComponent"; 344 345 public Vector2D offset; 346 347 this() { 348 this(0, 0); 349 } 350 351 this(double x, double y) { 352 offset = Vector2D(x, y); 353 } 354 355 this(Vector2D offset) { 356 this.offset = offset; 357 } 358 359 public string[string] getSnapshotData() { 360 return ["offset": to!string(offset)]; 361 } 362 } 363 364 class TextureCenteredDrawingOffset2DComponent : EntityComponent { 365 mixin EntityComponentIdentity!"TextureCenteredDrawingOffset2DComponent"; 366 } 367 368 class TextureCenteredDrawingOffsetProcessor : EntityProcessor { 369 public override bool acceptsEntity(Entity entity) { 370 return entity.hasComponent!DrawingOffset2DComponent 371 && entity.hasComponent!TextureCenteredDrawingOffset2DComponent 372 && entity.hasComponent!TextureComponent; 373 } 374 375 public override void update() { 376 foreach (entity; entities) { 377 auto textureSize = entity.getFromComponent!TextureComponent( 378 c => c.texture.getTextureSize()); 379 double xOffset = (cast(double) textureSize.width / 2) * -1; 380 double yOffset = (cast(double) textureSize.height / 2) * -1; 381 entity.withComponent!DrawingOffset2DComponent((c) { 382 c.offset = Vector2D(xOffset, yOffset); 383 }); 384 } 385 } 386 } 387 388 class Shader { 389 private File _shaderFile; 390 private ShaderType _type; 391 392 public @property File shaderFile() { 393 return _shaderFile; 394 } 395 396 public @property ShaderType type() { 397 return _type; 398 } 399 400 this(File shaderFile, ShaderType type) { 401 this._shaderFile = shaderFile; 402 this._type = type; 403 } 404 405 public abstract void compile(); 406 public abstract void destroy(); 407 } 408 409 class ShaderProgram { 410 protected Shader[] shaders; 411 protected bool _isCompiled = false; 412 413 public @property bool isCompiled() { 414 return _isCompiled; 415 } 416 417 this(Shader[] shaders) { 418 this.shaders = shaders; 419 } 420 421 public void compile() { 422 foreach (shader; shaders) { 423 shader.compile(); 424 } 425 _isCompiled = true; 426 } 427 428 public abstract void use(); 429 430 public void destroy() { 431 } 432 } 433 434 enum ShaderType { 435 vertexShader, 436 fragmentShader, 437 geometryShader, 438 tesselationControlShader, 439 tesselationEvaluationShader, 440 computeShader, 441 } 442 443 interface ShaderProgramFactory { 444 ShaderProgram createShaderProgram(); 445 } 446 447 class CachedShaderProgramFactory : ShaderProgramFactory { 448 private ShaderProgram shaderProgram; 449 450 protected abstract ShaderProgram create(); 451 452 public override ShaderProgram createShaderProgram() { 453 if (!shaderProgram) { 454 shaderProgram = create(); 455 } 456 return shaderProgram; 457 } 458 } 459 460 class ShaderProgramComponent : EntityComponent { 461 mixin EntityComponentIdentity!"ShaderProgramComponent"; 462 463 private ShaderProgram _shaderProgram; 464 465 public @property ShaderProgram shaderProgram() { 466 return _shaderProgram; 467 } 468 469 this(ShaderProgram shaderProgram) { 470 this._shaderProgram = shaderProgram; 471 } 472 } 473 474 class UnsupportedShaderTypeException : Exception { 475 this(ShaderType type) { 476 super("Unsupported shader type: " ~ to!string(type)); 477 } 478 } 479 480 class ShaderCompilationException : Exception { 481 mixin basicExceptionCtors; 482 } 483 484 struct ColorRgba { 485 ubyte r, g, b, a; 486 } 487 488 struct ColorRgb { 489 ubyte r, g, b; 490 } 491 492 class TextureColorComponent : EntityComponent, Snapshotable { 493 mixin EntityComponentIdentity!"TextureColorComponent"; 494 495 public ColorRgb color; 496 497 this(ColorRgb color) { 498 this.color = color; 499 } 500 501 public string[string] getSnapshotData() { 502 return [ 503 "r": to!string(color.r), 504 "g": to!string(color.g), 505 "b": to!string(color.b) 506 ]; 507 } 508 }