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.threedee.opengl.rendering;
13 
14 version(Have_derelict_gl3) {
15 
16 import retrograde.entity;
17 import retrograde.graphics.core;
18 import retrograde.file;
19 import retrograde.model;
20 import retrograde.graphics.threedee.opengl.shader;
21 import retrograde.graphics.threedee.opengl.model;
22 import retrograde.graphics.threedee.opengl.uniform;
23 import retrograde.math;
24 import retrograde.camera;
25 import retrograde.geometry;
26 import retrograde.graphics.threedee.opengl.error;
27 import retrograde.viewport;
28 
29 import poodinis;
30 
31 import derelict.opengl3.gl3;
32 
33 import std.string;
34 import std.algorithm;
35 import std.experimental.logger;
36 import std.exception;
37 
38 class OpenGl3RenderSystemInitException : Exception {
39     this(string reason) {
40         super("Could not initialize OpenGL render system: " ~ reason);
41     }
42 }
43 
44 class OpenGlRenderSystem : EntityProcessor {
45     private const GLVersion minimumOpenGlVersion = GLVersion.GL45;
46     private const string uniformModelViewProjectionName = "modelViewProjection";
47     private const string uniformIsTexturedName = "isTextured";
48     private static const uint[TextureFilterMode] textureFilterModeMapping;
49 
50     private GLVersion loadedOpenGlVersion;
51     private UniformBlock _retrogradeFramestateBlock = new UniformBlock("RetrogradeFramestate");
52     private UniformBlock _retrogradeModelstateBlock = new UniformBlock("RetrogradeModelstate");
53     private Matrix4D projectionMatrix;
54     private Entity cameraEntity;
55     private GLuint globalSampler;
56     private TextureFilterMode _globalTextureFilteringMode = TextureFilterMode.trilinear;
57     private Viewport viewport;
58 
59     @Autowire
60     private ShaderProgramFactory[] shaderProgramFactories;
61 
62     @Autowire
63     private SharedUniformBlockBuilder sharedUniformBlockBuilder;
64 
65     @Autowire
66     private Logger log;
67 
68     @Autowire
69     private ErrorService errorService;
70 
71     @Autowire
72     private ViewportFactory viewportFactory;
73 
74     public @property UniformBlock retrogradeFramestateBlock() {
75         return _retrogradeFramestateBlock;
76     }
77 
78     public @property UniformBlock retrogradeModelstateBlock() {
79         return _retrogradeModelstateBlock;
80     }
81 
82     public @property void globalTextureFilteringMode(TextureFilterMode textureFilterMode) {
83         this._globalTextureFilteringMode = textureFilterMode;
84         if (globalSampler) {
85             glSamplerParameteri(globalSampler, GL_TEXTURE_MIN_FILTER, textureFilterModeMapping[globalTextureFilteringMode]);
86             glSamplerParameteri(globalSampler, GL_TEXTURE_MAG_FILTER, textureFilterModeMapping[globalTextureFilteringMode]);
87         }
88     }
89 
90     public @property TextureFilterMode globalTextureFilteringMode() {
91         return _globalTextureFilteringMode;
92     }
93 
94     public static this() {
95         textureFilterModeMapping = [
96             TextureFilterMode.nearestNeighbor: GL_NEAREST,
97             TextureFilterMode.linear: GL_LINEAR,
98             TextureFilterMode.trilinear: GL_LINEAR_MIPMAP_LINEAR
99         ];
100     }
101 
102     public override void initialize() {
103         viewport = viewportFactory.create();
104 
105         loadedOpenGlVersion = DerelictGL3.reload(minimumOpenGlVersion);
106 
107         //TODO: Log loaded GL version
108 
109         glEnable(GL_CULL_FACE);
110         glEnable(GL_DEPTH_TEST);
111         glDepthFunc(GL_LEQUAL);
112 
113         initializeShaderPrograms();
114         initializeSamplers();
115         updateProjectionMatrix(); // TODO: Only update on resize of window/context
116 
117         errorService.throwOnErrors!OpenGl3RenderSystemInitException;
118     }
119 
120     private void initializeSamplers() {
121         glCreateSamplers(1, &globalSampler);
122         glSamplerParameteri(globalSampler, GL_TEXTURE_MIN_FILTER, textureFilterModeMapping[globalTextureFilteringMode]);
123         auto magFilter = globalTextureFilteringMode == TextureFilterMode.trilinear ? GL_LINEAR : textureFilterModeMapping[globalTextureFilteringMode];
124         glSamplerParameteri(globalSampler, GL_TEXTURE_MAG_FILTER, magFilter);
125         glBindSampler(0, globalSampler);
126     }
127 
128     private void initializeShaderPrograms() {
129         foreach (factory; shaderProgramFactories) {
130             auto shaderProgram = cast(OpenGlShaderProgram) factory.createShaderProgram();
131             if (shaderProgram && !shaderProgram.isCompiled) {
132                 shaderProgram.compile();
133             }
134         }
135     }
136 
137     protected override void processAcceptedEntity(Entity entity) {
138         if (entity.hasComponent!CameraComponent) {
139             cameraEntity = entity;
140         }
141     }
142 
143     protected override void processRemovedEntity(Entity entity) {
144         if (entity is cameraEntity) {
145             cameraEntity = null;
146         }
147     }
148 
149     public override void draw() {
150         GLfloat[] clearColor = [0.25, 0.25, 0.25, 1];
151         glClearBufferfv(GL_COLOR, 0, clearColor.ptr);
152         glClearBufferfi(GL_DEPTH_STENCIL, 0, 1, 0);
153 
154         auto viewProjectionMatrix = projectionMatrix * createViewMatrix();
155 
156         foreach (entity; _entities.getAll()) {
157             if (entity is cameraEntity) {
158                 continue;
159             }
160 
161             auto position = entity.getFromComponent!Position3DComponent(c => c.position, Vector3D(0, 0, 0));
162             auto orientation = entity.getFromComponent!OrientationR3Component(c => c.orientation, QuaternionD.nullRotation);
163             auto scale = entity.getFromComponent!Scale3DComponent(c => c.scale, Vector3D(1, 1, 1));
164             auto modelMatrix = position.toTranslationMatrix() * orientation.toRotationMatrix() * scale.toScalingMatrix();
165             _retrogradeModelstateBlock.uniforms.set(Uniform(uniformModelViewProjectionName, viewProjectionMatrix * modelMatrix));
166 
167             auto model = cast(OpenGlModel) entity.getFromComponent!ModelComponent(c => c.model);
168             if (!model.isLoadedIntoVram()) {
169                 model.loadIntoVram();
170                 errorService.logErrorsIfAny();
171             }
172 
173             bool hasTexture = false;
174             if (entity.hasComponent!TextureComponent) {
175                 auto texture = cast(OpenGlTexture) entity.getFromComponent!TextureComponent(c => c.texture);
176                 if (texture) {
177                     if (!texture.isLoadedIntoVram()) {
178                         texture.loadIntoVram();
179                         errorService.logErrorsIfAny();
180                     }
181 
182                     hasTexture = true;
183                     texture.applyTexture();
184                 }
185             }
186 
187             _retrogradeModelstateBlock.uniforms.set(Uniform(uniformIsTexturedName, hasTexture));
188 
189             if (entity.hasComponent!ShaderProgramComponent) {
190                 auto shaderProgram = cast(OpenGlShaderProgram) entity.getFromComponent!ShaderProgramComponent(c => c.shaderProgram);
191                 if (shaderProgram) {
192                     useShaderProgram(shaderProgram);
193                 }
194             } else {
195                 glUseProgram(0);
196             }
197 
198             model.draw();
199         }
200 
201         viewport.swapBuffers();
202     }
203 
204     private Matrix4D createViewMatrix() {
205         if (!cameraEntity) {
206             return Matrix4D.identity;
207         }
208 
209         auto position = cameraEntity.getFromComponent!Position3DComponent(c => c.position, Vector3D(0));
210 
211         if (cameraEntity.hasComponent!PitchYawComponent) {
212             auto pitchYaw = cameraEntity.getFromComponent!PitchYawComponent(c => c.pitchYawVector);
213             return createFirstPersonViewMatrix(position, pitchYaw.x, pitchYaw.y);
214         }
215 
216         auto orientation = cameraEntity.getFromComponent!OrientationR3Component(c => c.orientation, QuaternionD.createRotation(0, standardUpVector.vector));
217         return position.toTranslationMatrix() * orientation.toRotationMatrix();
218     }
219 
220     private void useShaderProgram(OpenGlShaderProgram program) {
221         foreach(uniformBlock; program.uniformBlocks) {
222             if (uniformBlock is null) {
223                 continue;
224             }
225 
226             try {
227                 sharedUniformBlockBuilder.buildData(uniformBlock, program);
228             } catch (UniformBlockBuildException e) {
229                 log.error(e.msg);
230             }
231 
232             program.bindUniformBlock(uniformBlock);
233         }
234 
235         program.use();
236     }
237 
238     public override void cleanup() {
239         viewport.cleanup();
240     }
241 
242     public override bool acceptsEntity(Entity entity) {
243         if (entity.hasComponent!RenderableModelComponent && entity.hasComponent!ModelComponent) {
244             auto model = cast(OpenGlModel) entity.getFromComponent!ModelComponent(c => c.model);
245             return model !is null;
246         }
247 
248         return entity.hasComponent!CameraComponent;
249     }
250 
251     private void updateProjectionMatrix() {
252         auto viewportDimensions = viewport.dimensions;
253         double aspectRatio = cast(double) viewportDimensions.width / cast(double) viewportDimensions.height;
254         //TODO: Make fov configurable
255         projectionMatrix = createPerspectiveMatrix(45, aspectRatio, 0.1, 1_000);
256     }
257 
258 }
259 
260 class OpenGlTexture : Texture {
261     private RectangleU dimensions;
262     private GLuint textureObject;
263     private ubyte[] texelData;
264     private bool loadedIntoVram = false;
265     private bool generateMipMaps;
266     private string textureName;
267 
268     this(ubyte[] texelData, RectangleU dimensions, bool generateMipMaps = true, string textureName = "") {
269         enforce!Exception(texelData.length % 4 == 0, "textelData should be aligned to 4-byte values (32bit). Does it contain the proper data format?");
270         this.texelData = texelData;
271         this.dimensions = dimensions;
272         this.generateMipMaps = generateMipMaps;
273         this.textureName = textureName;
274     }
275 
276     public void loadIntoVram() {
277         if (loadedIntoVram) return;
278 
279         glCreateTextures(GL_TEXTURE_2D, 1, &textureObject);
280         glBindTexture(GL_TEXTURE_2D, textureObject);
281         glTextureStorage2D(textureObject, 1, GL_RGBA8, dimensions.width, dimensions.height);
282         glTextureSubImage2D(textureObject, 0, dimensions.x, dimensions.y, dimensions.width, dimensions.height, GL_RGBA, GL_UNSIGNED_BYTE, texelData.ptr);
283 
284         if (generateMipMaps) {
285             glGenerateTextureMipmap(textureObject);
286         }
287 
288         texelData = [];
289         loadedIntoVram = true;
290     }
291 
292     public bool isLoadedIntoVram() {
293         return loadedIntoVram;
294     }
295 
296     public void applyTexture() {
297         glBindTexture(GL_TEXTURE_2D, textureObject);
298     }
299 
300     public override RectangleU getTextureSize() {
301         return dimensions;
302     }
303 
304     public override string getName() {
305         return textureName;
306     }
307 }
308 
309 }