Legend of Faerghail Remake

Community Forums/Showcase/Legend of Faerghail Remake

Krischan(Posted May) [#1]




My current project beside Extrasolar is a old one - I started 8 years ago with simple Blitz3D tests and now it's becoming better and better. A remake of the old Amiga RPG "Legend of Faerghail" from 1990. Well, I'm getting very familar with OpenB3D now, got my shaders running and I'm still optimizing the BSP loader. At the moment I only have a few screenshots to show the progress, it's still not optimized enough to post a running demo (click images for high resolution). But a demo will follow later when I think its ok, promised.

The torchlight is actually a type 3 spotlight which looks much better than a type 2, all textures use normalmaps and I have a lot of alpha brush models and additional custom B3D models loaded in this level (the knights and the chandliers are plain B3D models (with a shader, too) while the piano and the table are precalculated in the BSP map with a lightmap assigned). I'm not using any dynamic lights except the player torch and a "ambient light torch" to lighten up very dark areas with a decent dark blue tone. The candles, the torches and the fireplaces are simple sprites, not optimized in a single surface mesh yet. The level is absolutely dark except where your "torch" lights it up or the lightmap exists, hard to describe but looks very realistic.

The demo currently runs at 2560x1440 with 280 FPS and has been created using Microsoft Excel ;-) for the basic 2D level structure, a highly sophisticated CSV to MAP tool I wrote myself and the detailed work / compile has been done with a Netradiant fork and Q3MAP2. I'm using 105 different shaded surfaces right now but hope to decrease this number implementing a better shader cache for the BSP meshes like I already did with the B3D models. See some statistics below the screenshots.

Oh and compare the screenshots with my old Blitzmax Demo using DOT3 lights I made two years ago, which was too colorful and limited to vertex lighting.



Excel view of the level (just basic infos and height information are set)


Output of my CSV2MAP tool (starting a the green square at the top, looking south). All Walls, Doors, Event fields, treasure chests and lights are places automatically by just parsing the CSV file


If you're interested how it's done here is the Blitz3D source of it (still hadn't time to convert this to BMAX)

You can download the Excel Map and the CSV Output.

Netradiant view of the level


BSP Size: 6.820.596 Bytes

Parsing BSP Entities...
1 worldspawn
249 misc_gamemodel
46 func_door
67 info_event
3 info_darkness
3 info_vortex
3 info_trap
4 info_stair
266 light
1 info_player_start
3 fireplace
9 info_candle
10 func_brush
665 Entities found.

Final Map statistics:
Total BSP Surfaces: 94
Total BSP Vertices: 136149
Total BSP Triangles: 45383
Total BSP Models: 57
Solid BSP Level Meshes: 7
Alpha BSP Level Meshes: 50
Level Shaders: 94
Ingame Models (unique): 8
Ingame Models (reused): 241 in Instances
Model Shaders (unique): 11
Model Shaders (reused): 274
Torch Sprites: 16
Candle Sprites: 257
Fireplace Sprites: 3
Parse Time: 831ms
Assemble Time: 189ms
Total Build Time: 1020ms


Rick Nasher(Posted May) [#2]
Can't wait to see a demo(you know I love your work) ;-)


Ian Thompson(Posted May) [#3]
Look great Krischan, very atmospheric!


Matty(Posted May) [#4]
Fantastic


therevills(Posted May) [#5]
Wow!

Is OpenB3D a version of MiniB3D for BMax?


RemiD(Posted May) [#6]
I like it, it reminds me of Thief 1/2 ambiance (with better graphics)


Steve Elliott(Posted May) [#7]
Looking good :)


Chalky(Posted May) [#8]
Incredible - looks amazing!


AdamStrange(Posted May) [#9]
looking forward to seeing this :)


Krischan(Posted May) [#10]
@therevills: well, at least it shares the same syntax. If you know miniB3D you can get into OpenB3D very quick, too.

Thanks for the comments, I really appreciate your feedback. But currently I'm desperate because I can only use one lightmap using shaders. To speed things up I implemented a shadercache and it works really good but see yourself:

Brushtexture Lightmap


Shader Lightmap


With the "classic" brush to surface method the lightmap (which consists of two separate textures stored in the BSP_LightmapHandle[BSP_FaceLightmap[i], 0] handle) aligns perfect. BSP_FaceLightmap[i] stores the index [0...x] of the lightmap and the BSP_LightmapHandle[] stores the corresponding texture loaded. If I uncomment the last line with "ShadeSurface" command the lightmap is broken, it looks like only Lightmap index 0 is applied while there are 0 and 1.

The problem can be "bypassed" increasing the lightmapsize so that only one lightmap exists but this is only a temporary workaround and no solution I'd prefer.

I really don't get it where the problem is, do you have an idea what I've missed here? I don't understand why the brush works and the shader doesn't even they are both using the same variables set before.

I've added the BSP shaders, too. There, the "lmCoords" variable holds the Texture Coordinates (Set 1) and the "lightMap" variable in the fragment shader the lightmap texture set in the shader. I'm not sure if I implemented this correct, I couldn't find any example in the OpenB3D package so I had to perform try and error until it worked.

The Mesh Assemble Method from my BSP loader
	' --------------------------------------------------------------------------------
	' assemble map submodel
	' --------------------------------------------------------------------------------
	Method Q3BSP_AssembleModel:Int(num:Int)
	
		Local i:Int, t:Int, surf:TSurface, e:Int, newsurf:Int, texname:String
		Local index:Int, indexb:Int, indexc:Int, indexd:Int
		Local V0:Int, V1:Int, V2:Int
		Local sx:Float, sy:Float, sz:Float
		Local texture:TTexture
		Local normalmap:TTexture
		Local lightmap:TTexture
		
		Local brush:TBrush
		
		Local Class:Int = Null
	
		BSP_Model[num] = CreateMesh()
								
		For i = BSP_ModelFacestart[num] To BSP_ModelFaceend[num]
		
			texname = BSP_TextureName[BSP_FaceTexture[i]]
			
			' Flag for Alpha Models
			If Instr(texname, "alpha") Then Class = 1
		
			' skip skybox
			If (Not Instr(texname, "sky")) Then
			
				' always create a new surface
				newsurf = 1
				
				lightmap = BSP_LightmapHandle[BSP_FaceLightmap[i], 0]
							
				' or recycle available surfaces
				For t = 1 To BSP_SurfaceCount[num]
								
					' Texture and Lightmap match? recycle!
					If BSP_SurfaceTexture[t] = BSP_FaceTexture[i] And BSP_SurfaceLightmap[t] = BSP_FaceLightmap[i] Then
					
						surf = GetSurface(BSP_model[num], t)						
						newsurf = 0
						Exit
					
					EndIf
				
				Next
								
				' still new surface?
				If newsurf Then
				
					brush = CreateBrush()
				
					' create a new surface
					surf = CreateSurface(BSP_Model[num], brush)
					BSP_SurfaceCount[num] = BSP_SurfaceCount[num] + 1
					BSP_SurfaceTexture[BSP_SurfaceCount[num]] = BSP_FaceTexture[i]
					BSP_SurfaceLightmap[BSP_SurfaceCount[num]] = BSP_FaceLightmap[i]
				
				EndIf

				' set faceindex start
				index = BSP_FaceVertex[i]
			
				' build the mesh
				For e = BSP_FaceMeshVertex[i] To BSP_FaceMeshVertex[i] + BSP_FaceMeshNumber[i] - 1 Step 3
				
					indexb = index + BSP_MeshVertexOffset[e + 0]
					indexc = index + BSP_MeshVertexOffset[e + 1]
					indexd = index + BSP_MeshVertexOffset[e + 2]
				
					' position correction
					sx = -BSP_ModelBoxXstart[num] - (BSP_ModelScaleX[num] / 2.0)
					sy = -BSP_ModelBoxYstart[num] - (BSP_ModelScaleY[num] / 2.0)
					sz = -BSP_ModelBoxZstart[num] - (BSP_ModelScaleZ[num] / 2.0)
				
					' vertices
					V0 = AddVertex(surf, BSP_VertexCoordX[indexb] + sx, BSP_VertexCoordZ[indexb] + sy, BSP_VertexCoordY[indexb] + sz, BSP_VertexTexCoordU[indexb], BSP_VertexTexCoordV[indexb])
					V1 = AddVertex(surf, BSP_VertexCoordX[indexc] + sx, BSP_VertexCoordZ[indexc] + sy, BSP_VertexCoordY[indexc] + sz, BSP_VertexTexCoordU[indexc], BSP_VertexTexCoordV[indexc])
					V2 = AddVertex(surf, BSP_VertexCoordX[indexd] + sx, BSP_VertexCoordZ[indexd] + sy, BSP_VertexCoordY[indexd] + sz, BSP_VertexTexCoordU[indexd], BSP_VertexTexCoordV[indexd])
				
					' vertex colors
					VertexColor surf, V0, BSP_VertexRed[indexb], BSP_VertexGreen[indexb], BSP_VertexBlue[indexb]
					VertexColor surf, V1, BSP_VertexRed[indexc], BSP_VertexGreen[indexc], BSP_VertexBlue[indexc]
					VertexColor surf, V2, BSP_VertexRed[indexd], BSP_VertexGreen[indexd], BSP_VertexBlue[indexd]

					' vertex normals
					VertexNormal surf, V0, BSP_VertexNX[indexb], BSP_VertexNZ[indexb], BSP_VertexNY[indexb]
					VertexNormal surf, V1, BSP_VertexNX[indexc], BSP_VertexNZ[indexc], BSP_VertexNY[indexc]
					VertexNormal surf, V2, BSP_VertexNX[indexd], BSP_VertexNZ[indexd], BSP_VertexNY[indexd]

					' second coordinate set (lightmap)
					VertexTexCoords surf, V0, BSP_VertexLightmapCoordU[indexb], BSP_VertexLightmapCoordV[indexb], 1, 1
					VertexTexCoords surf, V1, BSP_VertexLightmapCoordU[indexc], BSP_VertexLightmapCoordV[indexc], 1, 1
					VertexTexCoords surf, V2, BSP_VertexLightmapCoordU[indexd], BSP_VertexLightmapCoordV[indexd], 1, 1
					
					' combine vertices to triangles				
					AddTriangle(surf, V0, V1, V2)
			
				Next

				' set second UV set for lightmap texture coordinates
				TextureCoords lightmap, 1

				' check for texture
				For Local tx:TBSPTexture = EachIn TBSPTexture.list

					' texture found?
					If tx.Name = texname Then
															
						' prepare for shader use
						texture = tx.id
						normalmap = tx.nm

						' has the shader already been used before?
						If MapValueForKey(ShaderCache, tx.id) Then
												
							sc = TBSPshadercache(MapValueForKey(ShaderCache, tx.id))
							ShaderCacheReused:+1
						
						Else
						
							sc = New TBSPshadercache
					
							' Create the Shader from the preloaded files to speed this up
							sc.shader = CreateShader("", BSP_VertShader, BSP_FragShader)
							SetFloat4(sc.shader, "tangent", 1.0, 1.0, 1.0, 1.0)
							SetFloat3(sc.shader, "emission", 0.015, 0.015, 0.015)
							SetFloat(sc.shader, "attspec", 0.0025)
							SetFloat3(sc.shader, "fogColor", FogColor[0], FogColor[1], FogColor[2])
					
							' different specularity for alpha models
							If Class = 1 Then SetFloat(sc.shader, "attspec", 0.005)

							' Apply Level Shader
							ShaderTexture(sc.shader, texture, "diffuseMap", 0)
							ShaderTexture(sc.shader, normalmap, "normalMap", 1)
							ShaderTexture(sc.shader, lightmap, "lightMap", 2)
																	
							' count shaders
							ShaderCacheUnique:+1
							
							' store Type for shader recycling
							MapInsert(ShaderCache, tx.id, sc)
						
						EndIf
						
					EndIf

				Next
				
				' old method to paint the surface with a brush (works)
				BrushTexture brush, lightmap
				PaintSurface surf, brush
				
				' new method to paint the surface with a shader (broken)
				ShadeSurface(surf, sc.shader)

			EndIf
					
		Next
						
		Return Class
	
	End Method


The Vertex Shader


The Fragment Shader



Grisu(Posted May) [#11]
Looks great!

Would a transition to BMX help to speed things up a bit?


Krischan(Posted May) [#12]
Ehm Grisu, this IS already done in BMX :-p


Grisu(Posted May) [#13]
Oh... so you already support DirectX11 as well?


Krischan(Posted May) [#14]
No, OpenB3D is based on OpenGL. As you can read here: https://sourceforge.net/projects/minib3d/

"OpenB3d is a library that allows easy access to 3d features of OpenGL, and quick game development"


markcw(Posted May) [#15]
Nice work Krischan!

I'm not really familiar with BSP but did you consider using something like distance to determine which lightmap is used for each area?

I'm not sure how far on Kfprimm is with it now but MaxB3d has both DX and GL drivers.


Krischan(Posted May) [#16]
Mark, I don't understand what you mean with the "lightmap distance". I think it is either a logical problem I've overseen in the Method I've posted how to apply the lightmap to the shader or a problem within the shader itself. As I wrote, the BrushTexture/Paintsurface stuff works fine, so I guess the Method is ok.

BSP is a very simple file format, the faces are indexed with a lightmap index ID and the lightmap UVs are stored together with the vertices using the second texture coordinate set, nothing special here. It could be the call of the Texturecoordinates in the vertex shader but I'm not very experienced in GLSL shader programming yet to debug this on my own. I've already read tons of tutorials and documentation but couldn't really find a solution for this problem.

Oh by the way - walking through the level looks much better than the screenshots can show :-D I still need to add some features, embed everything in my MaxGUI Framework and add other props but anyway I made a short sneak preview clip running through a part of the level (I hope you don't get dizzy of the unoptimized camera movement and the 115° FOV :-) The video is limited to 30FPS while the demo could run up to 280FPS currently. I didn't implement any timing yet, so this is plain spaghetti code :-D But in this video you should clearly see all Shader effects done with OpenB3D (the Torchlight / Bumpmapping and the Lightning strikes and even the fireplace animation is a spritesheet passed to a shader). The hardest part are good normal maps like the floor texture while I'm not very happy with the wall and the ceiling but that's the best I found so far.

Legend of Faerghail Sneak Preview (the demo looks much better than this video, I don't know why my youtube uploads always look messy compared to the original)


For the engine I think I'll stay with OpenGL as it is a open standard and works on Linux and MacOS, too. And so I would really appreciate if OpenB3D gets developed further. IMHO it is a real successor of miniB3D and the first easy to use 3D engine for Blitzmax since then :-)


Yue(Posted May) [#17]
Nice work !!!


Grisu(Posted May) [#18]
Awesome video! - The footsteps sound a bit strange though.


Krischan(Posted May) [#19]
By the way - here is an example how I upscaled the Amiga "texture" to use it in my engine. I want the original art of the game but more polished without losing the original texture character. So I experimented a long time and came up with this 6-step process. I started with a rip of the original image from WinUAE:

1. Original "Texture" 120x119 Pixel


2. Upscaled using ImageResizer and its XBR 4x filter applied (2x) and downsized it from 1920x1920 to 1024x1024 Pixel


3. Running my 1st Photoshop Action script to improve it further


4. Running my 2nd Photoshop Action script to add more details


5. Adding details / ageing using Genetica 4.0 (Basic Edition) and some trim textures from textures.com (this is the final Diffusemap). I'm sure you can achieve similar effects using the free Neo Texture Edit tool.


6. Creating a Normalmap from this Diffusemap with the free NormalMap-Online tool


Easy, huh? ;-)


Krischan(Posted May) [#20]
The footstep is a mix of the original footstep sound from the Amiga remastered in Stereo. I think it sounds strange to you because of the 30 FPS recording (the timing code playing the sample doesn't match the movement correctly below 60 FPS). I think in a final game you could choose between the Retro sounds and optimized sounds :-D


markcw(Posted May) [#21]
Oh sorry, I am a bit rusty with GL currently having been away from coding for many months, so please ignore my lightmap distance suggestion!

I've never looked at ShadeSurface, so don't have anything to say, perhaps you should define texcoords 1 in the shader? It could also be a bug, as I remember there are some.

Have you been using the version by DruggedBunny or Spinduluz? I will probably take a month or two to get round to an update.

Thanks for the video, the bumpmap is looking good! I wish you success for it. Also thanks for the how-to information!


degac(Posted June) [#22]
Wow, impressive!


Krischan(Posted June) [#23]
I'm using your latest wrapper (including the OpenB3D source included in it) from here only.


Steve Elliott(Posted June) [#24]

I want the original art of the game but more polished without losing the original texture character. So I experimented a long time and came up with this 6-step process.



Very impressive. Character not lost, and no artwork to be produced! :)


Krischan(Posted June) [#25]
Well, the downside is that this HQ wall texture uses more diskspace than the whole Amiga game occupied on three SD floppy disks (a 24bit uncompressed TGA uses 3.145.746 Bytes and one ADF floppy image has 901.120 x 3 = 2.703.360 Bytes for the whole game) :-p And we have to multiplicate the TGA size x 2 because there is a normal map, too!

But hey, it really looks better than 120x119 Pixels with 4 colors ;-)


Rick Nasher(Posted June) [#26]
Great graphics and atmosphere! Very creapy. Especially in the darker sections it feels like you can expect something to jump at you any second.
All I missed was a thunder and wind blowing sfx hehehe.


Krischan(Posted June) [#27]
Umm Rick, did you watch the video? There are lightnings, thunder, wind blowing and rain SFX :-)


Rick Nasher(Posted June) [#28]
So true. In beginning of video I hear the thunder sound indeed, at which point I previously had the audio muted, but further down the video there's no thunder/weather sound anymore so missed that before - silly me.


Krischan(Posted June) [#29]
Unfortunately the sound quality of the video is very bad, I used Fraps here and the stereo grab of the windows sound card sounds different compared to the demo (in the video it sounds like you are inside a large metal tank).


Rick Nasher(Posted June) [#30]
Ah, that's a pity then.


Yue(Posted June) [#31]
Badicam It is faster than fraps.
is Free.
https://www.bandicam.com/


xlsior(Posted June) [#32]
FWIW, Windows 10 has (game) screen recording built-in if your graphics card is good enough.


Krischan(Posted June) [#33]
Ah you mean the XBOX app - I tried it but it sounds similar. I really don't know why - perhaps I'm using too many channels at the same time (currently 9, with the brl.DirectSoundAudio import) but the demo sounds normal.

Which sound driver do you prefer?


Hardcoal(Posted June) [#34]
great.. stuff.. impressed..