Idea was to make sequal for my previous jam game: https://gm48.net/game/1197/lighthouse
though I got more interested playing around with shaders, so didn't manage to do any gameplay. So this is more of tech demo, trying out normal mapping. Game camera is 3D with orthographic projection, and then sprites are billboarded towards camera.

This video shows pretty much there is to it:


Some things

  • You can Hold TAB to see Normal map and without lighting effect.
  • Hold TAB and press "1" to change between different resolutions.
  • You can run with Shift
  • You can Fullscreen with F4, but be warned, I think GMS2.3 has bug which atleast crashes my computer when going from Fullscreen-to-Window.


Here is about how I approached drawing stuff, this isn't tutorial, but how I went and did things. And this is bit simplified and what I can rememeber from memory.

Light
In short: Every object draws itself on surface, and in other surface their normal maps. These are combined in shader. More complicated:

  • Game uses normal maps to give depth to flat sprites. Here is short thing about normal maps: https://i.imgur.com/8dekLER.png

  • Codewise Every object has own two draw-methods created in Creation-event. Draw-event is empty (only // comment).

  • For example:

    • For basic: "Draw = function() {draw_self(); } ", this method can have more drawing stuff too.
    • For Normal: "DrawNormal = function() {draw_sprite_ext(...); } ", and choose Normal-map sprite.
  • Have two global priority lists: first for basic drawing, and other Normal-map.

  • In step-events every drawable object adds themselves on these two priority lists -> Calls global function DrawSelfNormal();

  • Inside this function is something like this:

    • "function DrawSelfNormal() { var _priority = point_distance_3d(x,y,z,obj_cam.x,obj_cam.y,obj_cam.z); ds_priority_add(global.list_draw_basic, id, _priority); ds_priority_add(global.list_draw_normal, id, _priority); } "
  • Function adds just instance's id for drawing. It calculates priority, which is the depth-sorting.

  • Now in draw-control object in loop through these lists to draw them in depth-sorted order.

  • Draw-control has two surfaces for basic and normal-maps, but I didn't draw directly to them.

  • draw_clear(c_black) to empty application_surface, then draw all normal-map events.

    • In loop we find instance id like "var inst = ds_priority_find_max(...)" and then draw by "inst.DrawNormal();".
  • Now I copy application_surface to normal-map surface. We are done and our surface is normal-map

  • Clear application surface, and then draw basic things like same. I copied this to another surface.

  • I also added player high-light to third surface. Highligths would have been used for another thing too.

  • In GUI -event draw application surface with normal-map shader. I made shader also cut values, so light edge is more clear.

  • Colors are added by drawing over colored rectangle with gpu_set_blendmode();

  • About shader I'll write later.



Shadows
In short, I didn't use ray-casting, because my initial try with it was too jittery. So I made hacky way:

  • Every object which casts shadow draws primitive. Primitive consists two triangles.
  • Every object has starting width for shadow, and has two vertices next to it, always angled towards light.
  • Two other vertices are calculated from direction from light to these vertices.
  • Distance is given large enough number so shadow end doesn't appear in screen. https://i.imgur.com/v8sGhYZ.png

    During jam I just googled stuff etc. and one article I would to bring up is this: https://www.gamasutra.com/blogs/SvyatoslavCherkasov/20181023/329151/Graveyard_Keeper_How_the_graphics_effects_are_made.php

You must be logged in to leave feedback
Log in Register an account