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 }