ClassiCube icon indicating copy to clipboard operation
ClassiCube copied to clipboard

Contributing procedural fire texture to wiki?

Open coyo-t opened this issue 4 years ago • 15 comments

Hi. I've reverse engineered the fire texture (and converted it to C) and was wondering how I would go about adding that to the wiki? I've also reverse engineered the nether portal texture, but Im not sure if that one needs to be added or not?

thanks

coyo-t avatar Jun 20 '21 00:06 coyo-t

Nether portal isn't used in ClassiCube, but fire would be pretty cool to have added. You could probably just post your writeup in this issue and one of us could get it into the wiki.

Goodlyay avatar Jun 20 '21 02:06 Goodlyay

Should I also post the C conversion I made? Or just the texture generation pseudocode

coyo-t avatar Jun 20 '21 02:06 coyo-t

The purpose of having someone else reverse engineer it is so that UnknownShadow200, who has never looked at the original source code, can then implement his own version without any danger of plagiarism accusations. So I imagine it would make more sense to only post the pseudocode here, and UnknownShadow200 can then interpret that himself.

Goodlyay avatar Jun 20 '21 02:06 Goodlyay

IMPORTANT TYPO EDIT: mote_decay_amp should be 1.06, not 1.6. I'm very sorry. I apologize if this is a little hard to read/follow, im not 100% adept at writing pseudocode and making sure there are little to no misunderstandings the original fire textures were 16x16 with their backing data buffers at 16x20, however if you want fire to not look stretched on the block itself, you should increase it to 16x20 and 16x24 respectively (at the cost of both fire textures taking up a 16x32 area on the texture page)

additionally, this isnt a direct 1:1 port of the disassembly. I've put magic numbers into their own variables, given things names that (i think?) make sense, split things into smaller chunks, and converted logic into math where possible. so if you wanted water to be rendered with fire's colours, you could if you really wanted to.

struct colour {
	u8 r, g, b, a
}

struct fire_texture_binder {
	int gutter_size = 4

	// the gutter is used for the random noise generated at the bottom
	// that keeps the fire alive. this wont be seen on the final
	// texture as its outside the texture bounds
	int width, height + gutter_size

	int   mote_neighbor_decay_base = 18
	int   mote_neighbor_decay_rate = 1
	float mote_decay_amp = 1.06
	
	// mote decay was originally computed in the convolute function
	// by adding to it repeatedly, however it can be pretty easilly
	// calculated on its own ...if a bit long-winded (oops)
	
	float mote_decay =  mote_neighbor_decay_base
	      mote_decay *= mote_neighbor_decay_rate * mote_decay_amp
	      mote_decay *= (kernel_width * 2 + 1) * (kernel_height + 1)

	// width is a bit of a misnomer, since its subtracted and added to the x loc
	// ie w = 1: xxx, w = 2: xxxxx
	// but i cant really think of a better name
        // for reference, fire uses the convolution matrix of
        // 0, 0, 0,
        // 1, 1, 1,
        // 0, 1, 0
        // with anything outside the bounds being ignored
	int kernel_width  = 1
	int kernel_height = 1

	// in reality these are just one long buffer
	// but i think visualizing it as an array of arrays accessed with [x, y]
	// rather than [y * w + x] is easier
	float[height][width] front_buffer // all values are initialised to 0.0
	float[height][width] back_buffer
}


float function fire_life ():
	// assume random returns a number between 0. and 1. inclusive
	return random() * random() * random() * 4.0 + random() * 0.1 + 0.2


float function fire_convolute (fire, x, y):
	int kw, kh = fire.kernel_width, fire.kernel_height
	float new_mote = fire.front_buffer[x, (y + 1) % fire.height]
	new_mote *= fire.mote_neighbor_decay_base

	for (u = x - kw, u <= x + kw):
		for (v = y, v <= y + kh):
			if ((0 <= u < fire.width) and (0 <= v < fire.height)):
				new_mote += fire.front_buffer[u, v]

	return new_mote


function fire_advance (fire):
	// having the horizontal axis on the outside is intentional
	// as we want to work from the top downwards
	for (x = 0, x < fire.width):
		for (y = 0; y < fire.height):
			if (y >= fire.height - 1):
				fire.back_buffer[x, y] = fire_life()
			else:
				fire.back_buffer[x, y] = fire_convolute(fire, x, y) / fire.mote_decay

	array_swap(fire.front_buffer, fire.back_buffer)


colour function make_fire_colour (float v):
	v = clamp(v * 1.8, 0., 1.);

	colour = {
		r: v * 155 + 100,
		g: v * v * 255,
		b: pow(v, 10) * 255,
		a: 255
	}

	clamp_rgba8(colour.rgba); // clamp each component between 0-255

	if (v < .5):
		colour.a = 0

	return colour


function fire_fill_bitmap_data (fire, bitmap):
	for (y = 0, y < bitmap.height):
		for (x = 0, x < bitmap.width):
			float v = fire.front_buffer[x, y]
			bitmap.data[x, y] = make_fire_colour(v)

coyo-t avatar Jun 20 '21 04:06 coyo-t

Oh one other thing, if you want to bake this to a nice looping texture (so that on web clients you can pre generate these on-startup and store them for later) my method for that is such: assume buffer is one contiguous array composed of previous frames from fire.back_buffer, but each is cropped to be the final texture dimensions

function blend_animation_data (float[] buffer, int frame_count, int blend_count, int width, int height):
	int pxcount = width * height;
	
	for (var i = 0; i < blend_count; ++i):
		float fac = i / blend_count;
		float dest   = pxcount * i;
		float source = (pxcount * frame_count) - (pxcount * blend_count) + dst;
		
		for (var ip = 0; ip < pxcount; ++ip):
			var dest_ofs = dst + ip;
			var srcpx  = buffer[src + ip];
			var dstpx  = buffer[dest_ofs];
			
			buffer[dest_ofs] = (dstpx * fac) + (srcpx * (1. - fac))
	
	return frame_count - blend_count; // this can be used afterwards to update the frame count

after which you can use this make the final baked animation strip the number of blend frames to use really is trial and error, but ive found 8-10 for fire is pretty imperceptible. this works with all of the other procedural textures btw, since they're also driven by float buffers.

again i apologise if its jank, pseudocode and C aren't my native languages, Gamemaker Language is (which really is an unholy fusion of C, C++, and JS but regardless)

coyo-t avatar Jun 20 '21 05:06 coyo-t

Made a preliminary attempt to implement but it didn't quite work, will have more to say after I give it a second go

UnknownShadow200 avatar Jun 20 '21 13:06 UnknownShadow200

Yes I'm terribly sorry, I wrote this up very late at night and halfway-through it clicked what was actually going on with the fire. At it's core the fire is driven forwards by a simple convolution matrix. The different variables I labled mote_ are actually ones relating to that kernel. mote_decay is all of the elements of the kernel added up and used to normalize the result (enhanced slightly by mote_decay_amp). It's still a little soupy in my head but that's the gist of it. Due to how its written

float new_mote = fire->front_buffer[(y + 1) % fire->h * fire->w + x] * fire->decay_base; // decay base is 18 here
float kernel_mag = fire->decay_base;
for (int u = x - 1; u <= x + 1; ++u)
{
    for (int v = y; v <= y + 1; ++v)
    {
        // ignore any pixels outside of the image bounds.
        if (u >= 0 && u < fire->w && v >= 0 && v < fire->height)
        {
            new_mote += fire->front_buffer[v * fire->w + u];
        }
        kernel_mag += 1
    }
}
fire->back_buffer[y * fire->w + x] = new_mote / (kernel_mag * fire->kernel_amp); // kernel amp is 1.06

I think this means it uses a kernel of

0.0, 0.0,  0.0,
1.0, 1.0,  1.0,
1.0, 19.0, 1.0

with final divisor of the kernel being 24 * 1.06 unless the pixel is at the bottom row, in which case it's just set to random noise according to the expression rand() * rand() * rand() * 4.0 + rand() * 0.1 + 0.2; which is why the (y + 1) % h will never result in a mote at the bottom of the screen accessing wrapping around and accessing one at the top, as no motes that low down actually have the kernel applied to them.

Other thought: I think both fire textures could share the same back buffer for writing the new motes to. Again I'm really sorry for causing any confusion. I've been working on this fire stuff (+ other proc tex) for a few months now and its not until explaining how it works does stuff start to actually click.

coyo-t avatar Jun 20 '21 21:06 coyo-t

Ah, it's adding to kernel_mag - got a mostly working version FireStuff branch, still needs further cleanup though

UnknownShadow200 avatar Jun 23 '21 12:06 UnknownShadow200

I'm unsure as to how I'd go about submitting code for that (most times I do that its been 1:1 with someone) A because there's actually supposed to be two fire textures, not sure if that was just left out for the draft rn. and B I thought of an optimization: You don't actually need an entire 2nd buffer. Since the convolution matrix never influences pixels directly above itself, a WIDTH length strip that's 1px high can be used instead. Perform the kernel operations on the front buffer EXCEPT the writes, do those to the row strip, and then at the end copy the data in the row strip to the row you're currently on. The only gotcha to worry about with that is flipping the current FireAnimation_Tick loop's order, which right now is column-row, but for this to work it needs to be row-column this single strip can be used by both fire buffers too. image

coyo-t avatar Jun 23 '21 20:06 coyo-t

I've created a pull request #862 doing the above + naming consistency, and (probably overly verbose) commenting. The comments can probably be later shuffled over the wiki? I haven't actively tested any of this but I think it should work fine, except maybe the two fire textures being unique. I'm unsure as to how Random_SeedFromCurrentTime works (does it add microseconds since application start?). Should be changed to use the explicit seed input one.

coyo-t avatar Jun 24 '21 00:06 coyo-t

I am very sorry for being vague and wasting your time, because I really do appreciate the documentation you've written here!

Was just commenting earlier that I am working on fire animation, but haven't completed yet (I will be working again on it tonight). But unfortunately I cannot accept code changes related to fire animation


I added a MC Fire animation page to wiki and it should be editable

Random_SeedFromCurrentTime seeds from current UTC time (converted to milliseconds), with the goal of ensuring that procedural water/lava animations are subtly different every time the game is run

UnknownShadow200 avatar Jun 24 '21 02:06 UnknownShadow200

Huh. Aite then, damn. I'll get to the wiki page at some point. I can also provide geometry used for all of fire's original models. Of which there is a lot. Due to the obnoxious amount of "add vert" function calls it uses it'd probably be best to just keep them in a dirt simple model file thats a glorified vertex buffer with a table of whats in it and where at the beginning. Or generate them at startup. The former is probably easier on all fronts. + it opens the door for other blocks having their own model files, which could be nifty, if only just for texture pack creators. image

coyo-t avatar Jun 24 '21 13:06 coyo-t

It's not as simple as hardcoding a special model for fire specifically. All of the blocks in the game must be compatible with BlockDefinitions. If you wanted a special model, you'd have to extend that specification to allow defining blocks with that more complex model type.

Goodlyay avatar Jun 24 '21 18:06 Goodlyay

Could have a table of function pointers for "Block Renderers", which are picked if Shape is greater than 16. So the offset becomes BlockRenderer[Shape - 16] (But that's still hard coded stuff) Or if shape is greater than 16, expect/ask for a packet that describes the model data (size, uncompressed size, vertex count, number of "materials" for blocks that use multiple textures, ect). If it turns out you already have this model data, don't ask for the model data to be sent over. These are just spitball ideas though, I don't know much about networking image

coyo-t avatar Jun 24 '21 23:06 coyo-t

Note that adding support for more complicated block models is somewhat difficult - currently block models are rendered in 4 completely separate ways (blocks in inventory, blocks in /model [block id], blocks in world, blocks in world using advanced lighting)

UnknownShadow200 avatar Jun 25 '21 13:06 UnknownShadow200