Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fog rendering and weather #201

Open
afritz1 opened this issue Apr 8, 2021 · 39 comments
Open

Fog rendering and weather #201

afritz1 opened this issue Apr 8, 2021 · 39 comments

Comments

@afritz1
Copy link
Owner

afritz1 commented Apr 8, 2021

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.

@afritz1 afritz1 added this to the 0.14.0 milestone Apr 8, 2021
@Allofich
Copy link
Collaborator

Allofich commented Apr 10, 2021

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

@afritz1
Copy link
Owner Author

afritz1 commented Apr 10, 2021

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
Copy link
Collaborator

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
Copy link
Owner Author

afritz1 commented Apr 10, 2021

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
Copy link
Collaborator

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
Copy link
Owner Author

afritz1 commented Apr 11, 2021

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

f2a5ee1

@Carmina16
Copy link
Collaborator

I described lightning effects in the wiki.

@afritz1
Copy link
Owner Author

afritz1 commented Apr 11, 2021

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
Copy link
Collaborator

Yes, from 18 to 6.

@Allofich
Copy link
Collaborator

Allofich commented Apr 11, 2021

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
Copy link
Owner Author

afritz1 commented Apr 11, 2021

I'm able to get accurate time precision with my Clock class. I use that for streetlights and things already.

@afritz1
Copy link
Owner Author

afritz1 commented Apr 26, 2021

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

@afritz1 afritz1 changed the title Fog rendering Fog rendering and weather Apr 26, 2021
@afritz1
Copy link
Owner Author

afritz1 commented May 1, 2021

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

@afritz1
Copy link
Owner Author

afritz1 commented May 3, 2021

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

@afritz1
Copy link
Owner Author

afritz1 commented May 5, 2021

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
Copy link
Collaborator

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
Copy link
Owner Author

afritz1 commented May 5, 2021

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
Copy link
Owner Author

afritz1 commented May 5, 2021

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
Copy link
Owner Author

afritz1 commented May 5, 2021

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
Copy link
Collaborator

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
Copy link
Owner Author

afritz1 commented May 7, 2021

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
Copy link
Owner Author

afritz1 commented May 7, 2021

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
Copy link
Collaborator

"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
Copy link
Collaborator

Allofich commented May 7, 2021

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
Copy link
Collaborator

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
Copy link
Owner Author

afritz1 commented May 8, 2021

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
Copy link
Owner Author

afritz1 commented May 8, 2021

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

@afritz1
Copy link
Owner Author

afritz1 commented May 8, 2021

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

@Carmina16
Copy link
Collaborator

Carmina16 commented May 8, 2021

Something like that:

val <- val  + delta
tmp <- val + error
map <- (tmp >> 8)
error <- tmp & 0xff

@afritz1
Copy link
Owner Author

afritz1 commented May 16, 2021

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.

@afritz1 afritz1 removed this from the 0.14.0 milestone Nov 8, 2021
@Allofich
Copy link
Collaborator

Allofich commented Apr 24, 2024

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
Copy link
Collaborator

Allofich commented Apr 27, 2024

@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
Copy link
Owner Author

afritz1 commented Apr 28, 2024

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

@DerfJagged
Copy link

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
Copy link
Collaborator

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
Copy link
Collaborator

Allofich commented Apr 28, 2024

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
Copy link
Owner Author

afritz1 commented Apr 28, 2024

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
Copy link
Owner Author

afritz1 commented Apr 28, 2024

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
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants