Skip to content

Conversation

@Feksaaargh
Copy link

@Feksaaargh Feksaaargh commented Oct 7, 2025

Adds an app to simulate moss on the go.

Features

  • Procedural moss generation
  • You can eat the moss
  • A random story, kinda?

Images

Screenshot of the moss simulator running in Infinisim and displaying a noisy green moss texture

Screenshot of the moss simulator running in Infinisim and displaying the text "You find a normal patch of moss, then devour it."

A PineTime watch showing moss sitting on top of a Lego planter box

Usage

A bit of text is shown to introduce each moss. Tap to continue. Let the moss grow, then drag your finger over the screen to eat it. Rinse and repeat. (Rinsing optional, you can overwater mosses.)

Details

There are four normal scenes in the moss simulator: Forest, Cave, Civilization, and House. After each moss, there is a chance to move to a neighboring scene. They are connected as such:

Cave <-> Forest <-> Civilization <-> House

Forest and Civilization share some mosses, but otherwise almost all mosses are unique to their scene. There are some rare mosses that can be found anywhere as well.

There is a debug scene select mode that is accessible by holding tap on the app icon when launching, and continuing to hold for at least 1 second after it launches (the screen will stay dark during this). When released, press the side button to cycle the selected scene. Tap the screen again to accept.

The debug scene select mode exposes two (or so) otherwise inaccessible scenes: Error and DbgAllMoss. Error is a catch-all and only contains a failsafe moss. DbgAllMoss goes through every moss in order, showing its index in the inbetween texts. There's something at the end of DbgAllMoss if you munch your way to it.

This app does not work in Infiniemu due to an 'unhandled exception vcmpe.f32'. It works correctly in Infinisim, but generation takes a consistent amount of time (texture generation is much faster, but screen drawing is slow) and eating is slow.

This app prevents the screen from sleeping as long as it is open.

The TextureGenerator object is by far the most interesting component in this app. All moss images are created using it.

TextureGenerator usage

TextureGenerator

The topmost object in this structure.

It contains only a single vector of TextureLayer objects, which will be described next. These TextureLayers describe each layer to put onto the image. So if you want some nice multilayered Perlin noise, you'd simply make several Perlin layers of decreasing scales.

To get the image out of it, use GetBlock(). You pass in a buffer of lv_coord_t and the bounds of the area you want. This portion of the texture is generated and put into the buffer, starting at index 0. The bounds are inclusive, so make sure your buffer is of adequate size!

You may additionally use GetPixel() on the TextureGenerator but GetBlock() is preferred since it forces a structure which allows Perlin noise generation to run faster (caches some useful values).

To add layers, pass a TextureLayer into AddTextureLayer(). The TextureLayer is copied when passed in, so you may reuse and modify a single TextureLayer for multiple similar layers.

See header file for exact functions and parameter explanations.

TextureLayer

The descriptors for each layer.

This consists primarily of a NoiseType (an enum describing what type this TextureLayer is) and a TextureLayerData[something]. The constructor requests an std::any which MUST be the correct TextureLayerData* type for the NoiseType, else an std::any_cast exception will occur when the layer is rendered.

The associations between NoiseType and TextureLayerData* are straightforward:

  • Blank -> TextureLayerDataBlank
  • Simple -> TextureLayerDataSimple
  • Perlin -> TextureLayerDataPerlin
  • ShapeSquare -> TextureLayerDataSquare
  • ShapeTriangle -> TextureLayerDataTriangle
  • ShapeCircle -> TextureLayerDataCircle

The TextureLayerData* objects will described in the next section.

You can set the bounds of what is being rendered with the chainable function SetBounds(). This sets the min and max pixel to render IN SCREENSPACE. This has nothing to do with the offsets in the TextureLayerData* objects. If a pixel is outside the bounds (note that bounds are inclusive), the underlying texture will just not be calculated, so it can be used as an optimization tool as well as a tool for creating interesting visuals.

You should not need to get the values from any TextureLayers. However, the appropriate functions to do so would be CalculateLayer() and CalculatePixel(). They are similar to the TextureGenerator functions GetBlock() and GetPixel(), except CalculateLayer updates the current pixels in the buffer (taking opacity into account) rather than overwriting anything.

These objects are mostly used as a unified interface to all the different noise types there are in this generator system (hence the awkward use of std::any).

TextureLayerData*

These are the objects describing the parameters to each noise type.

Each layer type (except Blank) generates a value 0.0 - 1.0 for every pixel on the screen (depending on paramters), and uses that to get a value from a gradient described by a GradientData object (described in the next section).

TextureLayerDataBlank

A blank gray texture in Infinisim

TextureLayerDataBlank is the simplest, taking only a color and opacity. It fills in the entire screen with the given color. It is recommended to use a blank layer with full opacity as the bottommost layer as the background color.

Example code:

TextureLayerDataBlank layerData = TextureLayerDataBlank(LV_COLOR_GRAY, LV_OPA_100);
texGen.AddTextureLayer(TextureLayer(LayerNoise::Blank, layerData));

TextureLayerDataSimple

A pure noise texture in Infinisim

TextureLayerDataSimple generates a random value 0.0-1.0 for every pixel on screen, and interpolates the GradientData based on that.

Example code:

GradientData layerGrad = GradientData(0.0, 1.0, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100);
TextureLayerDataSimple layerData = TextureLayerDataSimple(layerGrad);
texGen.AddTextureLayer(TextureLayer(LayerNoise::Simple, layerData));

The below noise types all can be scaled and shifted separately on both the X and Y axes (since they are all deterministic). By default, the objects are created with identical scaling on X and Y, but this may be overridden with the chainable SetScale() function. The X and Y shifts are randomly generated upon object creation, but may be overridden with the chainable SetShift() function.

IMPORTANT: If the scale on an axis does not divide evenly into 65535, there will be a visible seam between pixels -32768 and 32767. All functions that set shift take this into account and if this seam would be visible, the screen's width/height (depending on axis) will be added to the shift. This can be an issue if using layers at slight offsets to each other, because if one would show the seam and another doesn't, the one that would show the seam will get shifted way off. An easy way to make a random offset that comfortably sits within the valid range is using std::rand() & 0x3FFF.

TextureLayerDataPerlin

A splotchy texture in Infinisim

TextureLayerDataPerlin generates Perlin noise. It is set seed, so shifts must be used to mimic random contents. Useful for organic looking things (like, well, moss). Useful tidbit: One quarter of pixels will be in range (0.4, 0.5) and another quarter will be in range (0.0, 0.4). This mirrors across 0.5.

Example code:

GradientData layerGrad = GradientData(0.0, 1.0, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100);
TextureLayerDataPerlin layerData = TextureLayerDataPerlin(layerGrad, 20);
texGen.AddTextureLayer(TextureLayer(LayerNoise::Perlin, layerData));

TextureLayerDataSquare

A square distance function texture in Infinisim

TextureLayerDataSquare is a distance function. It generates a grid pattern inside the X and Y scale, and effectively makes a square gradient coming out of the center of the square. Since it's just generating a value for each pixel, size isn't guaranteed and even if you give a very precise value for the start and end of the gradient, it could be a pixel off. Double check images in Infinisim if you need precise placement.

Example code:

GradientData layerGrad = GradientData(0.0, 1.0, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100);
TextureLayerDataSquare layerData = TextureLayerDataSquare(layerGrad, 60);
texGen.AddTextureLayer(TextureLayer(LayerNoise::ShapeSquare, layerData));

TextureLayerDataTriangle

A triangular distance function texture in Infinisim

TextureLayerDataTriangle is similar to TextureLayerDataSquare, except it generates triangles instead of squares. The gradient starts at 0.0 in the bottom middle (which is the base of the triangle) and increases as it goes to the top left and right corners. For perfect repeating triangles, the right side of the gradient should be at 0.5. For an equilateral triangle, X scale must equal Y scale * tan(30deg) * 2. Useful for squiggles and really any sort of sloped lines.

Example code:

GradientData layerGrad = GradientData(0.0, 1.0, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100);
TextureLayerDataTriangle layerData = TextureLayerDataTriangle(layerGrad, 60);
texGen.AddTextureLayer(TextureLayer(LayerNoise::ShapeTriangle, layerData));

TextureLayerDataCircle

A circular distance function texture in Infinisim

TextureLayerDataCircle is once again similar to TextureLayerDataSquare, but with a circle in a square. The gradient is circular of course, with 0.0 being the center and 1.0 being the outer four corners. For a perfect repeating circle inset in the square grid, the right side of the gradient should be at 1/sqrt(2) (use 1.f / std::numbers::sqrt2_v<float> (or just 0.7071 if you don't care for specificity)).

Example code:

GradientData layerGrad = GradientData(0.0, 1.0, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100);
TextureLayerDataCircle layerData = TextureLayerDataCircle(layerGrad, 60);
texGen.AddTextureLayer(TextureLayer(LayerNoise::ShapeCircle, layerData));

GradientData

This is the most important object in the TextureGenerator, as it defines the colors outputted for each noise type.

Each noise type returns a value in range [0.0, 1.0] for every pixel, the GradientData maps those values to usable colors to be painted.

A gradient is a linear RGB interpolation between the left and right colors and alphas (aka to and from, or low and high respectively). So if you have the left side be RGB green and the right side be RGB red, the middle will look brown. For values that lie OUTSIDE of the linear interpolation section, they are clamped to the nearer value (lower than the low endpoint will just be the low endpoint's color). This behavior can be changed with the SetClip(bool, bool) function. That sets if the left and right sides respectively should continue this behavior (false) or if they should be clipped to transparent (true). If an endpoint is at 0.0 or 1.0, I recommend setting the clip on that endpoint to false.

There are helper functions to reset the endpoint locations, colors, and alphas. There are also variants of the color and alpha resetting functions which only take a single value, and these set BOTH endpoints to the passed color/alpha. Having both endpoints be the same color/alpha is slightly faster than having them different, but I don't have any reason to believe that's an actual performance bottleneck.

Examples:

Square ring gradients on a blue background

// Blue background
texGen.AddTextureLayer(TextureLayer(LayerNoise::Blank, TextureLayerDataBlank(LV_COLOR_BLUE, LV_OPA_100)));
// Gradient which only populates the range 0.3-0.7. Outside of that range, it's clipped off (transparent).
GradientData squadient = GradientData(0.3, 0.7, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100).SetClip(true, true);
// Square texture layer which produces a square ring.
texGen.AddTextureLayer(TextureLayer(LayerNoise::ShapeSquare, TextureLayerDataSquare(squadient, 120)));

Triangular archways on a gray background

// Gray background
texGen.AddTextureLayer(TextureLayer(LayerNoise::Blank, TextureLayerDataBlank(LV_COLOR_GRAY, LV_OPA_100)));
// Gradient which only populates the range 0.3-0.5. Below that it's kept, but above that it's clipped.
GradientData tridient = GradientData(0.3, 0.5, LV_COLOR_BLACK, LV_OPA_100, LV_COLOR_WHITE, LV_OPA_100).SetClip(false, true);
// Triangle texture layer which produces a repeating set of archways.
texGen.AddTextureLayer(TextureLayer(LayerNoise::ShapeTriangle, TextureLayerDataTriangle(tridient, 80)));

See header file for exact functions and parameter explanations.


Whew, glad that explanation's over with.


Known issues

Receiving a notification while the app is running exits the app and loses all progress.

Final Notes

I am a well-adjusted person who can be trusted around moss.

(Please believe me.)

@Feksaaargh Feksaaargh changed the title Add Moss Simulator Moss Simulator App Oct 7, 2025
@github-actions
Copy link

github-actions bot commented Oct 7, 2025

Build size and comparison to main:

Section Size Difference
text 425684B 45552B
data 944B 0B
bss 22616B 72B

Run in InfiniEmu

@marigoldfish
Copy link

I was reading a nonfiction library book when I got the email notification there was activity in this repo. Then I had to laugh, because based on my library book, I think I am 100% the target audience for this app.

Screenshot_20251006-202239~2

@mark9064 mark9064 added the new app This thread is about a new app label Oct 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new app This thread is about a new app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants