afritz1 / OpenTESArena

Open-source re-implementation of The Elder Scrolls: Arena.
MIT License
915 stars 68 forks source link

Fog rendering and weather #201

Open afritz1 opened 3 years ago

afritz1 commented 3 years ago

In the original game, fog is an extra layer in screen space that follows the player around and acts like a mask over the scene. fog I experimented with FOG.TXT a bit. After some trial and error, I got some decent results by treating it as an uncompressed 128x128 image, two bytes per pixel, and dividing each pixel value by 4096 to get an intensity percent. screenshot000 This doesn't seem to be what the original game uses, and it was mentioned on Discord that the game uses a very complex noise algorithm instead.

Allofich commented 3 years ago

I don't know about FOG.TXT, but the original game does appear to load the FOG.LGT file.

afritz1 commented 3 years ago

I think FOG.TXT is actually used. Here are some notes from WalterPPK on Discord:

fog is generated by some very very complicated algorithm (akin to perlin noise?)

it seems the fog is a flat field around the player

you somehow project it to 40x25 cell on the screen and do some video memory manipulations with it

preliminarily, it seems to be a 'cube' of 3000x3000x1000 dimension, and each block is 2x2 pixels on fog.txt map it will take some time to make the heads and tails out of it: there is self-modifying assembly code and huge unrolled loops

FOG.LGT is a light table that tells what colors to use for darker areas. 13 palettes for 13 light levels.

Carmina16 commented 3 years ago

As a temporary implementation, you can generate a random 40x25 array of 0..MAX(FOG.LGT)>>8, zero out the 9th row, and draw a linear gradient for each 8x8 pixel block on the screen. Maybe dithering should be used to.

afritz1 commented 3 years ago

I will try that soon. I am figuring out rain and thunderstorms right now, it seems fairly easy except for the lightning bolt generation.

Allofich commented 3 years ago

I think FOG.TXT is actually used.

To test, I modified FOG.TXT in my directory and yes, it definitely is used. Modify the contents and you'll see artifacts appear in the fog in-game.

afritz1 commented 3 years ago

Need to add TXT file support to the texture manager (ironically). It's not really a text file.

https://github.com/afritz1/OpenTESArena/commit/f2a5ee16cd2f1cade49bace99879d2cc06b6cc95

Carmina16 commented 3 years ago

I described lightning effects in the wiki.

afritz1 commented 3 years ago

Thank you! I didn't know lightning bolts were just CFA files. I think I can get rain and thunderstorms done now.

Do thunderstorms only happen during the night? I haven't tested extensively yet but it felt like it was only at night.

Carmina16 commented 3 years ago

Yes, from 18 to 6.

Allofich commented 3 years ago

If you want to emulate the original game exactly, I believe it is when the game minutes passed that day are < 360 (6:00) or > 1081 (18:01).

Every time an in-game minute passes, a global value gets obtained like:

value = ((TotalGameMinutes % 1440) * 1024) / 1440;

which is the game minutes passed that game day, multiplied by 1024/1440.

Then, as a condition for doing the lightning bolts, this value needs to be either < 0x100 or > 0x300, which should translate to < 6:00 or > 18:01.

Of course, I imagine the intention was just basically getting it between 18 and 6.

afritz1 commented 3 years ago

I'm able to get accurate time precision with my Clock class. I use that for streetlights and things already. https://github.com/afritz1/OpenTESArena/blob/be8e83d6f035287b19925b45a37b7e41d2263d52/OpenTESArena/src/Game/ArenaClockUtils.h#L8

afritz1 commented 3 years ago

Rain and snow are done. Will be looking at thunderstorms next. I think I have everything I need, just need to do it.

afritz1 commented 3 years ago

Thunderstorms now have flashes and sound. Adding lightning bolts tomorrow probably.

afritz1 commented 3 years ago

Lightning bolts are working. Now this issue can be focused back on fog rendering.

afritz1 commented 3 years ago

As a temporary implementation, you can generate a random 40x25 array of 0..MAX(FOG.LGT)>>8, zero out the 9th row, and draw a linear gradient for each 8x8 pixel block on the screen. Maybe dithering should be used to.

This should be FOG.TXT right? I don't think we would get anything valuable by right shifting values from FOG.LGT since it's just palettes. If I do MAX(FOG.TXT) >> 8, that gives me 10.

Carmina16 commented 3 years ago

Sure; FOG.TXT contains 8.8 fixed point values that give the light level for the color transformation (0..13). So the highest 8 bits give the index, and the lower bits is error used for dithering.

afritz1 commented 3 years ago

Ah okay, I think it's coming together in my head now. Since MAX(FOG.TXT) >> 8 is 10, the light levels will be 0 to 9, which means the fog thickness can vary from completely transparent (palette index 0) to about 75% I guess. Also, the 40x25 effectively gets expanded to 320x200 because of the 8x8 pixel scaling w/ the gradient.

Working through it now trying to make more sense of it.

Haven't done any fixed-point stuff in this engine yet, or really ever.

afritz1 commented 3 years ago

Also, the random values in the 40x25 matrix is because we don't know how to interpret FOG.TXT due to a complicated algorithm and/or self-modifying assembly?

afritz1 commented 3 years ago

Finished the linear texture sampling code and I got this which looks promising. I had to convert the gradient calculation to true color for this example. Note the empty 9th row. screenshot000

Carmina16 commented 3 years ago

Yes, I am yet to guess the precise steps used for sampling. If you feel adventurous, try sampling every second pixel from a random position from FOG.TXT and move that position one pixel left and down each 16 frames.

afritz1 commented 3 years ago

I'm so close to being convinced that Arena uses some kind of spherical projection to map the fog onto the screen. If you turn the camera then the fog shimmers; parts of the texture appear and disappear. This makes me think it's a projection that's not 1-to-1 with the texture dimensions, and it's not always straight toward the texture.

The fog also has an every-other-pixel pattern to it like you mentioned.

I think there are two layers to the fog: the main fog texture and another one that slowly rotates around the player.

It would seem that, since the zeroed-out row is in the middle of the screen, then the bottom ~50 rows of the 320x200 texture are unused because that's where the interface is, or maybe they are used in a different way. This is a weird weather effect!

afritz1 commented 3 years ago

I just made one small discovery -- the right edge of the screen has a blocky appearance to it, like the 320x200 texture is not getting populated correctly from the noise algorithm, so it's like it's just the raw texels from the 40x25 texture.

Easier to see against a dark background like a wall.

Actually it looks like those tiles on the right edge are trying to wrap around the screen, like they are supposed to be 8 pixels left of the left screen edge instead. So this is probably an off-by-one bug.

So I have a theory now. For each 8x8 tile on the screen, Arena projects a square onto the 320x200 texture and reads those values onto the screen, and then it does the every-other-pixel masking to give it that dither pattern. Every 16 frames it also moves some position across the texture like you mentioned which adds to the "slowly crawling" look.

Carmina16 commented 3 years ago

"Every other pixel masking" is just dithering. As fog level is float, the float part is propagated to the pixel to the right, giving that specific pattern. And yes, the fog transformation is applied to the rendered world.

Allofich commented 3 years ago

I'm so close to being convinced that Arena uses some kind of spherical projection to map the fog onto the screen. If you turn the camera then the fog shimmers; parts of the texture appear and disappear. This makes me think it's a projection that's not 1-to-1 with the texture dimensions, and it's not always straight toward the texture.

It appears to depend on the player angle. Also, data from A.EXE is used.

I don't know if this will be helpful, as this is incomplete information, but in the function where FOG.TXT seems to be loaded, there is this:

MOV        SI,0x81d8                // Data at offset 0x47708 in the decompressed 1.06 A.EXE. Initial value is 0xFBFA.
MOV        BP,0x4
MOV        AX,word ptr [SI]      // Put the data at offset 0x47708  into AX
MOV        CX,word ptr [SI + 0x4]  // Put the value at 0x47712 (the value is 0x3E8, or 1000 in decimal) into CX
PUSH       SI
MOV        DI,0x1ff               // Put the value of 0x1FF (511 in decimal) into DI
SUB        DI,word ptr [0xc24c]      // Subtract the value at 0xc24c (likely player angle) from the value in DI (511)

A function is then called that uses those values in AX, CX and DI like follows:

ushort index = DI 2; ushort newAX = (2 AX array2[index]) + (2 CX -array1[index]); ushort newCX = (2 AX array1[index]) + (2 CX * array2[index]);

where array1 is an array of 16-bit values starting at 0x4A3D6 in the uncompressed 1.06 A.EXE, and array2 is an array of 16-bit values starting at 0x4A4D6.

array1's data looks like [0, 402, 804, 1206 ...] going up in intervals of 402 in the beginning, but smaller values later on, up to 32766. array2's data looks like [32767, 32766, 32759 ...], starting at 32767 and then seeming to be a reverse of array1.

The new values of AX and CX are then used in the fog function.

Carmina16 can probably explain further. Is this kind of incomplete information useful for you, afritz1? Should I continue trying to reverse the fog function?

Carmina16 commented 3 years ago

That is rotation by the player angle. I hope to produce the sampling part soon. The corresponfing drawing procedure (the one above) is already understood.

afritz1 commented 3 years ago

How do I use the low 8 bits of each FOG.TXT sample to do the dithering? I tried adding it as a delta but that just makes everything brighter and makes some pixels overflow to 0, resulting in black spots. Nevermind, I'm confused. The dithering is only in 320x200 space.

try sampling every second pixel from a random position from FOG.TXT and move that position one pixel left and down each 16 frames.

Does this mean generating a random XY position for every pixel, or does every pixel share that same one?

afritz1 commented 3 years ago

This is where I'm at. I tried the every-second-pixel pattern but not the movement yet. screenshot000

afritz1 commented 3 years ago

Going to try passing this data to the renderer next but still don't feel like I understand everything completely.

Carmina16 commented 3 years ago

Something like that:

val <- val  + delta
tmp <- val + error
map <- (tmp >> 8)
error <- tmp & 0xff
afritz1 commented 3 years ago

I'm thinking of merging what I have now to master and coming back to it later once we have more information on how it all works. I want to get started refactoring the UI since it's much larger of a task.

Fog is at least bootstrapped in the renderer now, which is valuable because the remaining work is more about implementation details and the overall data flow into the renderer will not be as affected by future changes. One of my goals was to get 100% of the necessary resources captured in the renderer so it's easier to refactor everything as a whole.

There is currently an issue with the clip space coordinates for the fog geometry. It does not project the fog texture properly if a corner of the fog cube is behind the camera. Need to figure that out at some point (basically just a math problem).

Feel free to make a Fog page or revise the Weather page in the wiki if there is anything you two would like to add.

Allofich commented 2 months ago

I added the function from A.EXE (1.06) that samples from FOG.TXT to the Weather page of the Wiki, translated to C++, as well as a helper function it relies on.

The function is called every game update (maybe only when rendering the 3d world) and two values that are increased by 4 just before every call (they are 0 at program start but are 4 by the time of the function's first call) have an effect on which data is sampled. Other than them, the player's X, Y, and angle affect the result, as does one more unknown value that might always be 0x92 when the function is called (it is modified by several parts of the game, being saved and restored when opening and closing menu screens, etc.)

Allofich commented 2 months ago

@afritz1 Added the next (last?) fog function as well, which uses the sampled data along with the contents of FOG.LGT. https://github.com/afritz1/OpenTESArena/wiki/Weather#fog

afritz1 commented 2 months ago

Thank you for exploring it in so much detail. I'll try to make sense of it after I get 0.15 out.

DerfJagged commented 2 months ago

Wow, that's some great detail. I believe the "changes every hour" part on the weather page may be inaccurate, as you can go in and out of any door repeatedly and it will change weather a few times within a couple real-world minutes even if the in-game hour does not increase.

Related to this thread, I was experimenting with the lighting palettes and was wondering if it might be possible to block the (annoying to some) effects of fog. I see I can replace FOG.LGT with a copy of NORMAL.LGT and the lighting acts as if it is a clear day, but the view distance is cut short.

Is there a way the files can be manipulated to remove the fog's view distance modifier perhaps by stubbing int SampleFOGTXT()? I think it would be great for a patch to remove this annoyance for the official game, it's something I'd be very interested in using for playing Arena casually!

Allofich commented 2 months ago

The functions, and FOG.TXT and FOG.LGT, probably aren't related to the drop in view distance, just to the fog effects that animate on the screen. (BTW I just changed "int SampleFOGTXT()" to "void SampleFOGTXT()" in the wiki since it isn't "returning" anything)

Allofich commented 2 months ago

Regarding the player angle, it may already be known, but the value is 0 at due south, 0x80 (128) at due west, 0x100 (256) at due north, and 0x180 (384) at due east. The maximum value it can hold is 0x1FF (511).

afritz1 commented 2 months ago

Is there a way the files can be manipulated to remove the fog's view distance modifier perhaps by stubbing int SampleFOGTXT()?

Arena implements fog as light, so it's actually player light distance that's limited. Not sure how to change this gameplay value, maybe Arena hardcodes it during daytime. Light spells help at night though.

afritz1 commented 2 months ago

Regarding the player angle, it may already be known, but the value is 0 at due south, 0x80 (128) at due west, 0x100 (256) at due north, and 0x180 (384) at due east. The maximum value it can hold is 0x1FF (511).

I'll make sure to normalize those rendering values 0->1 as much as I can so the math is in vector space and will work at any resolution.

Allofich commented 1 month ago

When writing the entry for the fog functions I wrote the locations of data in the 1.06 EXE as they would be in aExeStrings.txt. I had forgotten that data locations in the wiki were being written as the offset from the start of the decompressed A.EXE file (+0x3D30 as compared to the values in aExeStrings.txt). I just updated the fog functions entry to use offsets from the start of the file.