I replaced my zombies with some more zombie like models and started tweaking the graphics :)

Environment

My static environment is sharing a single lightmap texture which I baked in Blender. The static detail objects such as chairs and beds all share another lightmap. My lightmaps have a resolution of 4096x4096 and are later compressed using astc (with a block size of 6x6) for android and BC4 (the quality using BC1 was unusable) for windows/linux/macos. To do the lightmap UV layout I used the "Texture Atlas" plugin for blender (which I believe is a default plugin included in blender, but disabled).
The biggest challenge here are seams when mipmaps are being used in the distance, but wasting more texture space for bigger margins or using a mipmap bias for sampling the texture helps to hide those.

All lighting in this screenshot is baked:

Zombies

I wanted the Zombies to be effected by lights, one approach would be to check the lightmap below them and use that color, but getting that color takes a bit of work which I wasn't willing to put in at this time. Instead I made it easier by rendering another lightmap into a plane at a height of 1.5m inside the corridors. My levels are all on the same height anyway so just using a plane works great. I chose a plane that is big enough to fit any level and allows me to just use the texture directly in the Zombies fragment shader with hardcoded values for scaling and shifting so the texture pixels align with their correct position inside the level.

That texture is then sampled in the fragment shader and effects the ambient value and a very basic diffuse light source from above the zombie (all my lights are on the ceiling, this "light" is just hardcoded). I combine this with some weaker light from below the zombie as fake indirect light to make it look a bit more interesting.

The shader code for this looks like this:

float3 normalizedWorldNormal = normalize(vert.worldNormal);
float lights = saturate(normalizedWorldNormal.y) + saturate(-normalizedWorldNormal.y)*0.3; //These are the lights from above and below using lambert diffuse shading
float brightness = texture1.Sample(linearRepeatSampler, (-vert.worldPosition.xz + float2(38.0, -12.0))/80.0).r; //My plane is 80x80m and one of it's corners is at (38.0, -12.0)
float3 light = ambient * (brightness * 0.6 + 0.3) + lights * brightness * 2.0; //The final term to multiply with the zombies texture color, the numbers are the result of trial and error.

This is it in action:

0:00
/0:11

Zombies also have a drop shadow. I am not completely happy with how it looks and may end up tweaking it some more. It uses a light for each zombie that darkens the level geometry within it's radius. This can also be seen in the video above. My shader supports 8 of those.

Doors

I started out with having a fixed ambient value for every moving door which I manually assigned in my level json files. I came up with some acceptable values, but this does not adjust when moving the doors for example. The new idea here was to reuse the plane lightmap from above.
All I did for the doors was blurring that lightmap a tiny bit to remove some artifacts and use the lightmap pixels as ambient values for the door. There are lots of cases where this is not perfect and doors tend to be too bright from one side due to the lightmap rendering being without doors, but it looks much better than before.

0:00
/0:05

Floor

I've been experimenting with reflections using mirrored geometry and a transparent floor before, but the bigger level got too big for this to run smooth. Instead I am now using static cubemap reflections. The cubemap is rendered in blender using the "Render Cube Map" plugin (https://github.com/dfelinto/render_cube_map), I then use the command line tool "cmft" (https://github.com/dariomanesku/cmft) to blur it. And then a shader to mix these reflections into the floor based on angle using a fresnel approximation:

float3 incidentVector = normalize(vert.worldPosition - cameraPosition);
float3 normalizedWorldNormal = normalize(vert.worldNormal);
float3 reflectionDir = reflect(incidentVector, normalizedWorldNormal); //Reflect camera direction on the surface normal
float3 reflections = texture2.Sample(linearRepeatSampler, reflectionDir).rgb; //Get pixel position

float reflectionFactor = 0.3 * pow(1.0 + dot(incidentVector, normalizedWorldNormal), 5);
color.rgb = lerp(color.rgb, reflections, reflectionFactor);

Ideally the cubemap should be created from the camera position mirrored along the reflecting plane. In my case about 1.8m below the floor, but I had some problems with clipping away the floor in blender. Also this cubemap is not moving with the camera, so I chose a position in the middle of a corridor, which doesn't work that well for rooms. But making it a bit blurry helps with hiding how wrong it actually is and it looks a lot more interesting than without the reflections and is quite cheap.

0:00
/0:11

Particles

I also added some particle effects, but I am not exactly great at making those. One for breaking doors and another one for exploding zombies. I kinda like my broken door particle effect, while the other one is still not quite right. There is nothing very interesting about these. My particle system is generating a mesh for each particle material on the CPU where each particle has 4 vertices. Each vertex has the particles position, color and it's position in the quad. The vertex shader then transforms the vertices into a camera aligned quad and the fragment shader samples the texture and multiplies it with the vertex color. The result is a very flexible and decently fast particle system that works fine for low to medium amounts of particles.

Here is a video of all these changes together: