view: render map entities
[d2df-maped.git] / src / gfx.nim
blobaa6ab26bd94312626a565bdf2769554c75c07bb1
1 import
2   std/tables, std/sequtils, std/strutils,
3   sdl2, opengl, thirdparty/imgui, thirdparty/stb_image,
4   utils, vfs
6 export
7   sdl2, opengl
9 const
10   winDefaultX* = SDL_WINDOWPOS_CENTERED
11   winDefaultY* = SDL_WINDOWPOS_CENTERED
12   winDefaultW* = 1024
13   winDefaultH* = 768
14   winFlags = SDL_WINDOW_OPENGL or SDL_WINDOW_RESIZABLE or SDL_WINDOW_SHOWN
16 const
17   waterTextureNames* = ["_water_0", "_water_1", "_water_2"]
18   waterTextureColors*: array[3, DFColor] = [
19     (r: 0'u8, g: 0'u8, b: 255'u8, a: 128'u8),
20     (r: 0'u8, g: 255'u8, b: 0'u8, a: 128'u8),
21     (r: 255'u8, g: 0'u8, b: 0'u8, a: 128'u8)
22   ]
23   checkerSize* = 8
25 const
26   colBlack* = initDFColor(0x00, 0x00, 0x00, 0xFF)
27   colWhite* = initDFColor(0xFF, 0xFF, 0xFF, 0xFF)
28   colGrey* = initDFColor(200, 200, 200, 0xFF)
29   colDkGrey* = initDFColor(100, 100, 100, 0xFF)
30   colRed* = initDFColor(0xFF, 0x00, 0x00, 0xFF)
31   colBlue* = initDFColor(0x00, 0x00, 0xFF, 0xFF)
33 type
34   Texture* = ref object of RootObj
35     path*: string
36     glTex*: GLuint
37     glType*: GLenum
38     size*: DFSize
39     frames*: int # this is used for informational purposes only
41 var
42   sdlWindow: WindowPtr = nil
43   glContext: GlContextPtr = nil
44   textures: Table[string, Texture] = initTable[string, Texture]()
46 proc isTexture*(data: string): bool =
47   var ix, iy, icomp: int
48   result = stb_image.infoFromMemory(data, ix, iy, icomp)
50 proc isAnimTexture*(ar: FsArchive): bool =
51   (ar.findFile("TEXT/ANIM") != nil)
53 proc isAnimTexture*(stream: Stream): bool =
54   let pos = stream.getPosition()
55   var ar = vfs.openArchive("animtex", stream)
56   if ar != nil:
57     result = ar.isAnimTexture()
58     ar.close()
59     stream.setPosition(pos)
60   else:
61     result = false
63 proc isAnimTexture*(data: string): bool =
64   var stream = newStringStream(data)
65   result = isAnimTexture(stream)
66   stream.close()
68 proc newTexture(): Texture =
69   result = new(Texture)
70   result.path = ""
71   result.glTex = 0
72   result.frames = 1
73   glGenTextures(1, result.glTex.addr)
74   glBindTexture(GL_TEXTURE_2D, result.glTex)
75   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
76   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
77   glBindTexture(GL_TEXTURE_2D, 0)
79 proc newFillTexture(name: string, size: DFSize, color: DFColor): Texture =
80   result = newTexture()
81   result.path = name
82   result.size = size
83   result.glType = GL_RGBA
84   var buf = color.repeat(size.w * size.h)
85   glBindTexture(GL_TEXTURE_2D, result.glTex)
86   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA.GLint, size.w.GLsizei, size.h.GLsizei, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf[0].addr)
87   glBindTexture(GL_TEXTURE_2D, 0)
89 proc newCheckersTexture*(name: string, size: DFSize, color1: DFColor = colWhite, color2: DFColor = colGrey): Texture =
90   result = newTexture()
91   result.path = name
92   result.size = size
93   result.glType = GL_RGB
94   var buf = newSeq[tuple[r, g, b: uint8]](result.size.w * result.size.h)
95   for i, c in buf.mpairs():
96     let x = i mod result.size.w
97     let y = i div result.size.w
98     let rx = x mod (checkerSize * 2)
99     let ry = y mod (checkerSize * 2)
100     if (rx >= checkerSize) xor (ry >= checkerSize):
101       c.r = color1.r
102       c.g = color1.g
103       c.b = color1.b
104     else:
105       c.r = color2.r
106       c.g = color2.g
107       c.b = color2.b
108   glBindTexture(GL_TEXTURE_2D, result.glTex)
109   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB.GLint, result.size.w.GLsizei, result.size.h.GLsizei, 0, GL_RGB, GL_UNSIGNED_BYTE, buf[0].addr)
110   glBindTexture(GL_TEXTURE_2D, 0)
112 proc newErrorTexture(name: string): Texture =
113   result = newCheckersTexture(name, initDFSize(16, 16), colBlack, initDFColor(0xFF, 0x00, 0xFF, 0xFF))
115 proc newTexture*(name: string, contents: string, overrideSize: DFSize = initDFSize(0, 0)): Texture =
116   var ix, iy, icomp: int
117   if not stb_image.infoFromMemory(contents, ix, iy, icomp):
118     error("Could not read texture: ", name, " ", stb_image.failureReason())
119     return nil
121   result = newTexture()
122   result.path = name
124   let wantComp = (if icomp == 4: 4 else: 3)
125   var data = stb_image.loadFromMemory(contents, ix, iy, icomp, wantComp)
126   var dataPtr: pointer = nil
127   if (overrideSize.w > 0 and overrideSize.w < ix) or (overrideSize.h > 0 and overrideSize.h < iy):
128     var newSize = overrideSize
129     if newSize.w <= 0 or newSize.w > ix: newSize.w = ix
130     if newSize.h <= 0 or newSize.h > iy: newSize.h = iy
131     # cut out the top left part of the texture
132     let dstStride = newSize.w * wantComp
133     let srcStride = ix * wantComp
134     var buf = newString(newSize.w * newSize.h * wantComp + 1)
135     var src = 0
136     var dst = 0
137     for y in 0 ..< newSize.h:
138       copyMem(buf[dst].addr, data[src].addr, dstStride)
139       src += srcStride
140       dst += dstStride
141     result.size = newSize
142     dataPtr = buf[0].addr
143   else:
144     result.size = initDFSize(ix, iy)
145     dataPtr = data[0].addr
147   result.glType = (if wantComp == 4: GL_RGBA else: GL_RGB)
148   glBindTexture(GL_TEXTURE_2D, result.glTex)
149   glTexImage2D(GL_TEXTURE_2D, 0, result.glType.GLint, result.size.w.GLsizei, result.size.h.GLsizei, 0, result.glType, GL_UNSIGNED_BYTE, dataPtr)
150   glBindTexture(GL_TEXTURE_2D, 0)
152 proc newAnimTexture*(name: string, stream: Stream): Texture =
153   result = nil
155   var ar = vfs.openArchive(name, stream)
156   if ar == nil:
157     error("Could not load anim texture: ", name, " (file is not an archive)")
158     return
160   defer: ar.close()
162   var f = ar.readFile("TEXT/ANIM")
163   if f == nil:
164     error("Could not load anim texture: ", name, " (file doesn't have ANIM)")
165     return
167   var info = f.readDFConfig()
168   f.close()
170   if not ("resource" in info and "framewidth" in info and "frameheight" in info):
171     error("Could not load anim texture: ", name, " (file has invalid ANIM)")
172     return
173   
174   let resName = "TEXTURES/" & info["resource"]
175   f = ar.readFile(resName)
176   if f == nil:
177     error("Could not load anim texture: ", name, " (missing actual texture: ", resName, ")")
178     return
180   let contents = f.readAll()
181   f.close()
183   let overrideSize = initDFSize(info["framewidth"].parseInt(), info["frameheight"].parseInt())
185   result = newTexture(name, contents, overrideSize)
187   if result != nil and "framecount" in info:
188     result.frames = info["framecount"].parseInt()
190 proc newTexture*(path: string, overrideSize: DFSize = (0, 0)): Texture =
191   var f = vfs.open(path, false)
192   if f == nil:
193     error("Could not find texture: ", path)
194     return nil
195   if f.isAnimTexture():
196     result = newAnimTexture(path, f)
197   else:
198     let contents = f.readAll()
199     result = newTexture(path, contents, overrideSize)
200   f.close()
202 proc deinit*(self: Texture) =
203   if self.glTex != 0:
204     glDeleteTextures(1, self.glTex.addr)
205     self.glTex = 0
207 proc destroy*(self: Texture) =
208   assert(self.path in textures, "orphaned texture: " & self.path)
209   textures.del(self.path)
210   self.deinit()
212 proc destroyTexture*(name: string): bool {.discardable.} =
213   if name in textures:
214     textures[name].destroy()
215     result = true
216   else:
217     result = false
219 proc loadTexture*(name, contents: string): Texture {.discardable.} =
220   result = newTexture(name, contents)
221   if result != nil:
222     if name in textures:
223       textures[name].deinit()
224     textures[name] = result
226 proc loadTexture*(path: string): Texture {.discardable.} =
227   result = newTexture(path)
228   if result != nil:
229     if path in textures:
230       textures[path].deinit()
231     textures[path] = result
233 proc loadTextureExt*(path: string, name: string, overrideSize: DFSize = (0, 0)): Texture {.discardable.} =
234   result = newTexture(path, overrideSize)
235   if result != nil:
236     result.path = name
237     if name in textures:
238       textures[name].deinit()
239     textures[name] = result
241 proc getTexture*(name: string): Texture =
242   if name in textures:
243     return textures[name]
244   result = loadTexture(name)
246 proc shutdown*() =
247   imgui.shutdown()
249   for tex in textures.mvalues():
250     tex.deinit()
251     tex = nil
252   textures.clear()
254   if sdlWindow != nil:
255     if glContext != nil:
256       discard sdlWindow.glMakeCurrent(nil)
257       sdl2.glDeleteContext(glContext)
258       glContext = nil
259     sdlWindow.destroy()
260     sdlWindow = nil
261   sdl2.quit()
263 proc init*() =
264   if not sdl2.init(INIT_VIDEO or INIT_EVENTS):
265     error("Could not init SDL2: ", sdl2.getError())
266     quit(-1)
268   addExitProc(gfx.shutdown)
270   sdlWindow = sdl2.createWindow("maped", winDefaultX, winDefaultY, winDefaultW, winDefaultH, winFlags)
271   if sdlWindow == nil:
272     error("Could not create SDL2 window: ", sdl2.getError())
273     quit(-1)
275   # the "opengl 2.1" backend of imgui actually only uses 1.x features
276   discard sdl2.glSetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1)
277   discard sdl2.glSetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3)
278   discard sdl2.glSetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY)
279   glContext = sdlWindow.glCreateContext()
280   if glContext == nil:
281     error("Could not create SDL2 GL2.1 context: ", sdl2.getError())
282     quit(-1)
284   discard sdl2.glSetSwapInterval(1)
286   opengl.loadExtensions()
288   if not imgui.init(sdlWindow):
289     error("Could not init ImGui")
290     quit(-1)
292   # spawn special textures
293   textures["$$ERROR"] = newErrorTexture("$$ERROR")
294   for i, x in waterTextureNames:
295     textures[x] = newFillTexture(x, initDFSize(1, 1), waterTextureColors[i])
297   glDisable(GL_DEPTH_TEST)
298   glDisable(GL_ALPHA_TEST)
299   glDisable(GL_CULL_FACE)
300   glDisable(GL_STENCIL_TEST)
301   glDisable(GL_LIGHTING)
302   glDepthMask(GL_FALSE)
304 proc beginFrame*(): bool =
305   result = true
306   var event = sdl2.defaultEvent
307   while sdl2.pollEvent(event):
308     discard imgui.event(event)
309     if event.kind == QuitEvent:
310       return false
312   glDisable(GL_SCISSOR_TEST)
313   glClearColor(0, 0, 0, 0xFF)
314   glClear(GL_COLOR_BUFFER_BIT)
315   glEnable(GL_SCISSOR_TEST)
317   imgui.beginFrame()
319 proc endFrame*() =
320   imgui.endFrame()
321   sdlWindow.glSwapWindow()
323 proc vidWidth*(): int = sdlWindow.getSize()[0]
325 proc vidHeight*(): int = sdlWindow.getSize()[1]
327 proc vidSize*(): DFSize =
328   let s = sdlWindow.getSize()
329   result = (s[0].int, s[1].int)
331 template withBlending(on: bool, actions: untyped): untyped =
332   if on:
333     glEnable(GL_BLEND)
334     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
335   actions
336   if on:
337     glDisable(GL_BLEND)
339 template withTexture(tex: GLuint, actions: untyped): untyped =
340   if tex == 0:
341     glDisable(GL_TEXTURE_2D)
342   else:
343     glEnable(GL_TEXTURE_2D)
344     glBindTexture(GL_TEXTURE_2D, tex)
345   actions
346   glBindTexture(GL_TEXTURE_2D, 0.GLuint)
348 proc drawQuad*(pos: DFPoint, size: DFSize, tint: DFColor = colWhite) =
349   let x0 = pos.x.GLint
350   let y0 = pos.y.GLint
351   let x1 = (pos.x + size.w).GLint
352   let y1 = (pos.y + size.h).GLint
353   glColor4ub(tint.r, tint.g, tint.b, tint.a)
354   withTexture(0):
355     withBlending(tint.a != 0xFF'u8):
356       glBegin(GL_TRIANGLE_FAN)
357       glVertex2i(x0, y0)
358       glVertex2i(x1, y0)
359       glVertex2i(x1, y1)
360       glVertex2i(x0, y1)
361       glEnd()
363 proc drawQuad*(pos: DFPoint, size: DFSize, tex: Texture, tint: DFColor = colWhite, flipX: bool = false) =
364   let xn = ((if flipX: -size.w else: size.w) div tex.size.w).GLint
365   let yn = (size.h div tex.size.h).GLint
366   let x0 = pos.x.GLint
367   let y0 = pos.y.GLint
368   let x1 = (pos.x + size.w).GLint
369   let y1 = (pos.y + size.h).GLint
370   let blend = (tex.glType == GL_RGBA or tint.a != 0xFF'u8)
371   glColor4ub(tint.r, tint.g, tint.b, tint.a)
372   withTexture(tex.glTex):
373     withBlending(blend):
374       glBegin(GL_TRIANGLE_FAN)
375       glTexCoord2i(0, 0)
376       glVertex2i(x0, y0)
377       glTexCoord2i(xn, 0)
378       glVertex2i(x1, y0)
379       glTexCoord2i(xn, yn)
380       glVertex2i(x1, y1)
381       glTexCoord2i(0, yn)
382       glVertex2i(x0, y1)
383       glEnd()
385 proc correctLine(x1, y1, x2, y2: var GLint) =
386   # make lines only top-left/bottom-right and top-right/bottom-left
387   if y2 < y1:
388     swap(x1, x2)
389     swap(y1, y2)
390   # pixel perfect hack
391   if x1 < x2:
392     x2.inc()
393   else:
394     x1.inc()
395   y2.inc()
397 proc drawRect*(pos: DFPoint, size: DFSize, tint: DFColor) =
398   var x1 = pos.x.GLint
399   var y1 = pos.y.GLint
400   var x2 = (pos.x + size.w).GLint
401   var y2 = (pos.y + size.h).GLint
402   var nx1, ny1, nx2, ny2: GLint
403   let blend = (tint.a != 0xFF'u8)
404   if x1 > x2: swap(x1, x2)
405   if y1 > y2: swap(y1, y2)
406   glLineWidth(1f)
407   glColor4ub(tint.r, tint.g, tint.b, tint.a)
408   withTexture(0):
409     withBlending(blend):
410       glBegin(GL_LINES)
411       (nx1, ny1, nx2, ny2) = (x1, y1, x2, y1)
412       correctLine(nx1, ny1, nx2, ny2)
413       glVertex2i(nx1, ny1)
414       glVertex2i(nx2, ny2)
415       (nx1, ny1, nx2, ny2) = (x2, y1, x2, y2)
416       correctLine(nx1, ny1, nx2, ny2)
417       glVertex2i(nx1, ny1)
418       glVertex2i(nx2, ny2)
419       (nx1, ny1, nx2, ny2) = (x2, y2, x1, y2)
420       correctLine(nx1, ny1, nx2, ny2)
421       glVertex2i(nx1, ny1)
422       glVertex2i(nx2, ny2)
423       (nx1, ny1, nx2, ny2) = (x1, y2, x1, y1)
424       correctLine(nx1, ny1, nx2, ny2)
425       glVertex2i(nx1, ny1)
426       glVertex2i(nx2, ny2)
427       glEnd()
429 proc drawPoints*(pts: openArray[DFPoint], tint: DFColor, size: int = 1) =
430   glPointSize(size.GLfloat)
431   glColor4ub(tint.r, tint.g, tint.b, tint.a)
432   withTexture(0):
433     withBlending(tint.a != 0xFF):
434       glBegin(GL_POINTS)
435       for pt in pts:
436         glVertex2i(pt.x.GLint, pt.y.GLint)
437       glEnd()
439 proc drawPoint*(pt: DFPoint, tint: DFColor, size: int = 1) =
440   glPointSize(size.GLfloat)
441   glColor4ub(tint.r, tint.g, tint.b, tint.a)
442   withTexture(0):
443     withBlending(tint.a != 0xFF):
444       glBegin(GL_POINTS)
445       glVertex2i(pt.x.GLint, pt.y.GLint)
446       glEnd()
448 proc setViewport*(pos: DFPoint, size: DFSize) =
449   let y = GLint(vidHeight() - pos.y - size.h)
450   glScissor(pos.x.GLint, y, size.w.GLsizei, size.h.GLsizei)
451   glViewport(pos.x.GLint, y, size.w.GLsizei, size.h.GLsizei)
453   glMatrixMode(GL_PROJECTION)
454   glLoadIdentity()
455   glOrtho(0f, size.w.GLfloat, size.h.GLfloat, 0f, -1f, +1f)
457   glMatrixMode(GL_MODELVIEW)
458   glLoadIdentity()
460 proc setOffset*(pos: DFPoint) =
461   glMatrixMode(GL_MODELVIEW)
462   if pos.x == 0 and pos.y == 0:
463     glLoadIdentity()
464   else:
465     glTranslatef(-pos.x.GLfloat, -pos.y.GLfloat, 0f)
467 proc clear*(c: DFColor) =
468   glClearColor(c.r.GLfloat / 255f, c.g.GLfloat / 255f, c.b.GLfloat / 255f, 1f)
469   glClear(GL_COLOR_BUFFER_BIT)