Skip to content

v0.95.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 13:43
· 241 commits to master since this release

Added

  • A quality level sets how far plants keep their detail. A level's profile takes a detailScale (0.25 to 1; Low 0.5, Medium 0.8, High 1): a placed plant changes to its cheaper mesh, stops being drawn and stops casting a shadow at that share of its distances, so the same world draws a fraction of the triangles on a phone. It is a row in Project Settings → Rendering → Quality Levels.

  • Placed plants cast shadows only as far as twice their detailed distance. MeshInstances takes a shadowDistance (metres from the eye past which an instance casts no shadow); a plant kind with a cheaper mesh sets it to twice that mesh's distance, so a tree far off is drawn into the shadow maps no more than its shadow is worth.

  • The editor lets go of a generated world while the game plays. The game runs in a realm of its own that builds its own copy of a generated world; the editor's copy is released for as long as it runs and made again on Stop, so the two are not held together.

  • Plants are instances of one mesh. A generated world's and a PlacedFoliage's plants no longer merge their vertices a cell at a time: a new MeshInstances component places one resident mesh at many places (sixty-four bytes a plant), and the engine culls each plant against the view, takes a cheaper mesh past a distance and draws none past another, as one instanced draw that cells sharing a mesh and a material merge into. The geometry exists once however many plants there are, so a planet's plants take a tenth of the memory and a fraction of the draws, and a cell is grown in microseconds instead of a few milliseconds.

  • A project can keep folders out of its asset database. assetExclude in the project manifest names folders (a source-art repository, docs, a server) the asset scan does not index, does not write a .meta into and the editor does not watch.

  • Small plants need not cast a shadow. A plant part takes castShadows: false, so ground cover too small to be worth a shadow-map draw is left out of the shadow pass.

  • A project plugin that needs trust says so. Opening a project whose editor plugin has not been trusted shows a notice with a way to review it, instead of leaving its missing pieces (a ground that is not drawn) unexplained.

  • A game's generated ground shows in the editor. An editor plugin may now import the engine subpaths the editor is built on (esengine/terrain, esengine/worldgen, esengine/water, esengine/spline, esengine/tilemap, esengine/replication), as the editor's own instances, so a project plugin can register the terrain provider its game registers and the viewport draws the ground the game will. New projects' .gitignore leaves .esengine/plugins/ and .esengine/platforms/ versioned, as the plugin page always said, and ignores the typings and the session.

  • A lookup table is adopted as data. A PNG shaped like a colour lookup table (N slices of N by N: 1024 by 32, 256 by 16, 4096 by 64) is given no compression, no mips, no sRGB decode and clamped edges when it is added to a project, where it used to take the picture defaults and answer wrongly until someone found why.

  • An Outdoor Environment to create. Create → Rendering → Outdoor Environment makes a sun, a sky light, a clock, a weather and a global volume with the sky (clouds that cast shadows), the haze (beams in their gaps) and a plain grade, already pointing at one another. It is the arrangement the Terrain & Water example uses, for the engine's scale of a hundred units to the metre.

  • An Online 3D starter. A template for a networked 3D game: a lit ground, a pawn per player predicted on its owner's screen and replicated to the others, a camera that follows your own, and a server/ that runs the same project headless. The rules both ends agree on are one module, src/shared/rules.ts. tools/check-arena-server.mjs now takes --project, and runs against this template too.

  • Start a project from the command line. estella create <template> <dir> [--name] makes a project from any starter or example, and estella templates lists them. The editor's New project gallery and the command line now share one implementation (pipeline/src/project/scaffold.ts), so both copy the same files and write the same package.json and .gitignore.

  • Clouds drawn from a coverage picture. skyAtmosphere takes a Cloud Mask (u_cloudMask, with u_cloudMaskTile in kilometres of the cloud layer) and a second, higher one (u_cloudMask2): the clouds stand where the mask says and the noise only erodes them. The shadow the clouds cast and the beams of heightFog's cloudShade read the same masks, which the fog takes from the sky. Terrain & Water uses the two the art made.

  • A Display LUT. The displayLut effect reads a look off a 1024x32 lookup table by the picture as the display will show it, in the last pass, after the output transform and the transfer function: the place a look authored against the finished image belongs, and one no effect in the chain can be, since they run on scene values before the curve. It is a variant of the pass that was running, one texture read and no pass of its own, and PostProcessStack.setDisplayLut(texture, amount) is the same thing in code. The last pass now reads its parameters off the reflected layout as a material does, and the copies it makes share them.

  • Light beams that follow the clouds. heightFog has a cloudShade: its volumetric light marches the sun's ray up through the cloud layer of a skyAtmosphere whose clouds cast shadows (the same density the sky draws, fixed to the world and drifting with its wind), so a gap in the clouds is a beam in the air and what they cover is dark, whichever way the camera looks. volumeRange may now be as long as a million units, for beams through clouds kilometres up. The sky's clouds are copied onto the fog each frame, and an effect's uniform may be internal, which an inspector does not list.

  • Sparkle on the water. The Water shader reads two tiled normal maps that scroll over the surface (u_detailMap, u_detailTile0/1, u_detailStrength0/1, u_detailFlow0/1), packed into the one texture a material has room for, so the sun's glints on small slopes come from lit specular response and not a white texture; a material that sets no strength is unchanged.

  • A generated world's plants. A recipe's members can name a plant in the rules' plants (a .esworld file holds them): several parts (a trunk and its crown), a cheaper mesh and a far version, the colourings the art gives it, how far it is sunk and its unit (100 for metres in a world of centimetres). The world is laid out from the rules as the eye moves, the plants grown a cell at a time by the machinery a PlacedFoliage uses: a chunk stands at once and fills in over the frames after it inside the frame's budget, a cheaper mesh stands in at the middle distance, and a tree's crossed cards, laid out as a layer of their own, stand for it far off (a part's soft turns the normals of such a card toward up, so a crown's card is lit as a rounded crown and not a dark slab on the side away from the sun). A plant with no cards of its own may use its cheaper mesh as its far version and set farKeep, the share of its plants that version draws, so ground cover far off costs a share of what it does near; a member with attach: 'align' leans a plant with the ground, so a stone on a slope sits as the slope lies; the far versions are laid out in cells as wide as the shortest distance they are shown over allows, which took the Terrain & Water example's draws from about 1,800 to about 1,100. The terrain's paint is a field each of its layers (paint.grass), a rules file names noise fields of its own, and fields are worked out only where a condition reads them. The Terrain & Water example's woods, shrubs, stones, grass, flowers and reeds are laid out this way from assets/worlds/plants.esworld.

  • Each terrain layer says how it meets the others. A .esterrainlayer may name a heightBlend: 0 blends by the paint alone (grass into sand), 1 by the layers' heights (stones standing through grass), weighted by how much of each layer is there; absent, it is the material's u_heightBlend. The editor's layer panel sets it.

  • Placed plants. PlacedFoliage keeps every plant an artist placed by hand as data in one entity, a table of kinds and six numbers a plant, instead of an entity, a renderer and a draw each. The engine cuts the area into cells and grows only those round the eye, each as one mesh for every part of every kind in it, a few milliseconds a piece of the frame's budget so a cell fills in over the frames after it appears, and lets them go past their distance. A kind keeps the mesh as made (normal map, second uv, vertex colour), has a cheaper mesh for the middle distance and a far version, a tree's crossed cards, drawn out to a far distance: a hierarchical level of detail that makes the whole area stand at once. The Terrain & Water example now grows its woods, shrubs, stones, grass, flowers and reeds this way from the lakewood plant set, and its grass and sand are that set's terrain layers.

  • Water zones. Oceans, lakes and rivers are now drawn as one surface. A WaterBody says what water is where (its form, outline or Spline, shore width, current and how high its waves stand), and a new WaterZone bakes every body over its region into one field: the surface's height, the distance to the shore, the waves' height, the current and the depth over the terrain. One mesh of rings that follows the camera reads it, so a river runs into a lake or the sea at one height with its banks running on into the shore, its current fades into the still water, and no shore foams across its mouth; a lake or a river over the sea is the lake's, not the sea's, and the sea's waves calm and foam where it shallows over the terrain. The zone holds what the bodies share: the material, the waves' length, speed and heading, the mesh's detail and the ripples. A scene without one gets a zone over its lakes and rivers, and the ocean goes on past it. Waters.sample reads the same field, so what floats or looks out from under the water meets the surface that is drawn. In the editor the zone's region is outlined in the viewport.

  • Wind and rain on the water. The weather drives the wind: each preset has a wind and a wave strength (mist calms, a storm raises the waves to over twice their height), the Water Body's waves and the foliage's sway follow it in play, and a Weather that does not drive the wind (WeatherDrive.Wind) leaves them as authored. Rain falls on water that keeps ripples as rings, as hard as the weather rains, and the rain effect no longer lays a wet floor or puddles over the water itself.

  • Surf on the shore. Waves run in toward every shore of a water zone, stand up as the water shoals, break white and run up the ground past the waterline before falling back. WaterZone.surf sets how high a breaker stands against its body's waves and surfLength the length of the surf's waves; a current carries none, and Waters.sample rides the same surf. The zone now measures the distance to the shore across the ground for every body, an ocean over a beach included, where an ocean used to measure its depth.

  • Terrain without cracks, and without end. The ground is now a CDLOD quadtree round the eye: every node is the same small patch drawn as instances of one mesh, its heights and normals read on the GPU from pages of a height atlas, and its vertices slide onto the coarser grid toward the end of their level's range, so levels meet without cracks or skirts and nothing pops as the camera moves. Terrain.extent Infinite makes ground that goes on for ever from seeded noise read anywhere (worn by the noise's own erosion), drawn out to drawDistance; cellSize is the finest cell near the eye. Pages are made a few a frame, coarse and near first, and shown as progress.

  • Terrain from a generator the game names, versioned. Infinite ground is now made by a terrain provider: a function of the world position that samples whole regions (heights, and paint if it has its own), the same on every machine, and that may answer later from a worker or a server. Terrain.provider, world and generatorVersion name it; registerTerrainProvider adds a game's own, and the engine's noise is estella:noise version 1. A terrain whose provider is missing, whose version its provider no longer has, or whose heightmap cannot be read fails with a reason in Terrains.status and answers nothing, instead of growing noise in its place.

  • Terrain collision, the same ground that is drawn. 3D bodies and characters now collide with a Terrain: it is built in tiles round everything that moves on it (collision, collisionDistance, collisionLayer, friction), the tile under a body at once, and kept through a rebuild until the new ground replaces it. What is collided with, what Terrains.heightAt and normalAt answer and the finest level drawn near the eye are now one set of triangles, so a ray, a query and the picture agree wherever the camera is; Terrains.collisionReadyAt says when the ground under a point is ready. Any system that makes static geometry while the game runs can hand it to the 3D world through the new CollisionGeometry resource.

  • Procedural chunks, with one owner and one budget. What a game makes as it runs is now chunks of layers in ProceduralChunks (chunksOf(app)): each layer says how it is cut, how far round the streaming sources it is wanted and how a chunk is made, published and released, and the chunks are made nearest first within the frame's budget (the one Bakes.budgetMs, shared with the bakes and the terrain's pages), released with everything they published past the unload radius, and made again when the layer's generation changes, the old version staying until the new one replaces it; an answer for an older generation is thrown away. status and stateAt say where they stand. Terrain collision and foliage cells are layers: plants now grow round the eye out to drawDistance and are let go past it, and a changed field or ground regrows them without blinking out.

  • Generated worlds. The new esengine/worldgen lays a world out by a game's rules as it is explored: biomes weighed from fields (the terrain's height and slope, the water's depth, seeded moisture, temperature and rock, and a game's own), mixed at their edges and painting the terrain; recipes an artist composes (an anchor and members round it, with footprint apart from canopy) placed in layers, big things first, each keeping clear of what was placed before it; and WorldZones (a home, a road, a building) that keep out what they name and hold a biome, fading at their edge. The same seed lays out the same world in whatever order its chunks are made, every placement has an id that outlives rules added later, interactive ones are entities with a PlacedObject, and what players change (diffs().remove(id)) is kept apart and laid over before anything is shown. plan().explain(x, z) says why anything stands or does not. A new example, Generated World, walks through one.

  • Which water a place is. The sea fills only low ground joined to the open sea past the water zone's edge: a hollow below its level walled off by higher ground stays dry (or holds a lake of its own). A lake or a river is water only where its surface is over its ground, so a hill inside its outline is land, for what is drawn and what Waters.sample answers, whatever order the bodies were added in. WaterBody.bodyId names a body the same in every session; Waters.sample carries it and Waters.bodyAt(x, z) says which body a place belongs to. findBasins gives a game the ground's basins (each filled to the lowest point of its rim, with its outlet), the drainage down to the edge and the sea's reach, for deciding where its lakes and rivers go; drainPath follows the water from a point.

  • An area without a heightmap is a window on its provider's ground. Its noise is read from Terrain.provider where the area stands in the world, over the provider's height range, instead of being made in the area's own space and stretched to fill it: two areas of the same size side by side join, an area is the same ground its provider gives without end, and a game's own provider can make an area. Ground read from a provider is read again when its terrain is moved, so it stays the world's ground.

  • Rays against the terrain, anywhere. Terrains.raycast(origin, direction, maxDistance) meets the ground's triangles exactly (the ones collision and heightAt use) wherever the ray goes, with no physics world and no collision tile there, for a line of sight, a pick or a landing spot.

  • Far versions for generated decoration. A recipe member can name its far version (far: crossed cards, or a tent for a conifer, showing a rectangle of its texture), drawn past its layer's lodDistance, and decoration that leans with the ground (attach: 'align') now leans in its merged mesh too.

  • Terrain modifiers, and zones that level their ground. Terrains.setModifier lays a change over the ground (flattenModifier levels a circle or a turned box, blending back over its edge) that what is drawn, the queries, rays and collision all follow, remaking only the pages and collision tiles it reaches; heightBeneathAt reads the ground as it was made. A WorldZone with flatten levels its plot to the ground's height at its centre.

  • Terrain layers: up to eight kinds of ground, each with its normal and mask maps. A terrain layer (.esterrainlayer, with an editor of its own) names the albedo, normal and mask (occlusion, roughness, height) an artist made for one kind of ground, and how it is laid; a Terrain lists up to eight (layers), weighed by its splatmap and a second one (splatmap2). They are drawn by the new Terrain Layers shader from texture arrays: where layers meet the higher one shows, near the eye they are read without repeat, on slopes they are projected from the sides, and their normals, occlusion and roughness light the ground. Biomes can paint all eight. The four-layer Terrain shader is unchanged for materials that use it.

  • Foam is where waves burst, not along every shore. The zone bakes how steep each shore is: against rock and cliffs the shore foams and the breakers burst white; up a gentle beach the waves wash in a thin line only and run up a short way rather than spreading a white sheet over the sand; and the less room the wind has to raise them (a sheltered lake), the less of either.

  • Surf comes in from open water as lines. Its crests followed every turn of the shore, bending into a river's mouth from far out; they now follow a distance of their own, the open water's edge with a river's water taken for land, smoothed over the surf's length, so they come in straight and turn to the shore only as they near it. Where the surf rises, breaks and runs up is still the real shore's, and a river's current calms it over a spread past the mouth rather than cutting it off at the river's side.

  • A river gives way where it runs into the sea or a lake. Over half its width past their shore it fades into them, its current with it, where it used to stop within a texel and a half and leave its sides running on into the open water: the surf, which no current carries, broke off along those lines. The surf now runs on along the coast and round into the mouth.

  • Water meets the shore along a smooth line. Close up, a river's banks and the surf on a beach ran in steps a texel of the water zone wide: the baked shore distance is now stored finely near the shore (a signed square root of it), and past the shore the surf's film and the shallows' foam are told dry from wet by the picture's depth at each pixel rather than by the baked depth, which only knew whole texels.

  • The distance to the shore is a distance. Near the water's edge the baked shore distance was the water's depth (or, on land, the height above the water), so Waters.shoreAt, a generated world's shoreDistance and the surf's reach mixed heights with distances; the depth is now turned into the distance across the ground by the ground's slope there. Waves still calm as the water shallows.

  • Heightmaps say how they hold their heights. Terrain.heightmapFormat is Grey (the grey or red channel at the file's depth, so a 16-bit grey PNG, what terrain tools export, is read at full precision) or Red + green (sixteen bits across two 8-bit channels); the format is no longer guessed from the pixels. A heightmap that cannot be read as data says it must ship as the PNG it is. Upgrading: a terrain whose heightmap packs sixteen bits into red and green sets heightmapFormat to Red + green.

  • Rivers cut their beds. A river's WaterBody.bedDepth cuts its bed into the terrain, a trough under its surface with banks that blend back into the ground, laid as terrain modifiers along it so what is drawn, the queries and collision follow, and editing the spline remakes only the ground along the river.

  • Whether a place can be walked to. Terrains.findPath(from, to, { maxSlope, step, blocked }) searches the ground for a way a walker held to a slope can take, reading the ground where it goes (modifiers too, nothing baked), and returns it pulled straight with its length, or says whether there is no way, the search ran out, or an end stands on no ground: what a game checks a spawn or a goal against.

  • World rules as a file, with an editor. A WorldGen's rules can name a .esworld file: the biomes, recipes and layers as data, checked as they are read (a missing recipe or layer fails the world with where it went wrong), followed by the cook like a material so the meshes and materials it names ship with the scene, and watched so a saved edit lays the world out again. The editor opens it in a World Rules editor that lists the biomes, recipes and layers, edits each with a form, and says whether the engine would take the rules as they stand.

  • How far a place is from the water. Waters.shoreAt(x, z) gives the distance across the ground to the water's edge (above zero on dry land, below on the water), and generated worlds read it as the shoreDistance field, so a beach or a band of reeds can follow the shore.

  • A vertex hook can hand the fragment a colour. What a material's vertex hook leaves in es_vertexColor is now the colour its fragment receives (times the instance's tint), on both backends.

  • Bakes run over frames, with progress. Work that used to hold a frame until it was done (a terrain worn by rain, a water zone baked, terrain chunks built, plants grown) is now spread over frames within a budget, and listed with its progress in the new Bakes resource (bakesOf(app).active). The editor's status bar shows it as a bar, for the edit world and for the game running in Play, and a game can show it on a loading screen. Plants wait for the ground and the water they stand on instead of being grown twice.

  • Eroded and warped terrain. Terrain.erosion wears the ground with rain (a heightmap's as well as the noise's): gullies down the slopes, sharper ridges, silt in the valleys, and dirt painted where the water ran; the same seed wears the same ground. Terrain.warp bends the noise by noise of its own for winding valleys and bent ridges. erodeHeightfield and erosionJob do the same for a tool.

  • Terrain layers without repeat, and with natural edges. Each layer is read with hex tiling near the eye (every hexagonal cell takes it shifted and turned at random, the three about a point blended by distance and brightness), so no tile is seen; from the middle distance on it also mixes in itself on a tile u_farScale times as wide; and each layer's weight wanders by noise of its own (u_blendNoise), so where grass meets sand the line is ragged instead of the paint's gradient. u_antiTile now sets the hex tiling's strength.

  • A richer Terrain shader. Layers meet by their relief (u_heightBlend), each is read twice and blended by slow noise so no repeat is seen (u_antiTile), every layer gets the broad variation, their brightness is drawn into the light as relief (u_relief), and ground below u_wetLevel + u_wetBand darkens and shines at the waterline.

  • Water mirrors the world. Seen from above the surface now reflects what is on screen (hills, trees, the far shore) along its reflected ray, not only the sky, fading at the picture's edge; the quality level's mirror switch covers it.

  • Soft water edges. Where the water thins out at a shore the ground behind shows through more and more over the last u_edgeSoftness of depth, so the water meets it along a soft line instead of the zone's texel steps, and the shore's foam is broken up rather than drawn as an outline.

  • Plants keep out of the water. A Foliage area grows nothing under water unless waterDepth lets it stand that deep (reeds in the shallows), and is grown again when the water changes. Grass thins out blade by blade toward the distance its cells turn to their far version, so a near cell beside a far one is no longer three times as thick.

  • Hair with Blender's highlight. The Hair shader takes the Principled shader's highlight when u_anisotropy is above zero: GGX stretched along the strand as Blender stretches it, with u_specular as the specular factor. A hair card imported with an anisotropy (KHR_materials_anisotropy, which Blender exports from its Principled anisotropy) is set to it, so the broad sheen across a fringe that Blender draws is drawn in the same place.

  • Hair shadows and diffuse. A hair material draws the sun's shadow of hair as hair. The Hair shader gains u_hairShadow, how much light a layer of strands takes per world unit along it (declared with the new #pragma hairshadow), and while it is above zero the frame adds a cascade fitted to the hair alone as the first in the sun's list: cards a few millimetres apart fell inside one texel of a cascade spread over the view, so the layers could not shadow each other, and the face under a fringe is now shadowed at the hair's resolution too. The light left after a depth of hair falls off with that depth, from the nearest card to this one, as a deep shadow map reads it, with a card's own surface left out so it does not shadow itself. A hair material imported from a glTF is set to a density of 400. The Hair shader has u_strandDiffuse, how much of the diffuse is the strand's own Kajiya-Kay term against a surface's N dot L, and a hair material imported from a glTF sets it to 0. At the old 0.6 a card turned edge-on to the light still took a third of it, so every lock was lit to one tone and the gaps between locks never went dark; with the Principled shader's diffuse they do. With the lamp highlight at a tenth and the tinted second lobe at half what they were, the top of the hair comes out within about six percent of Blender's render of the same frame on both backends, where it was half again as bright.

  • Hair drawn as locks. The Hair shader can draw a card's strands as locks: u_clumpCount sets how many across its width, u_clumpTone how much lighter or darker one is than the next, u_clumpShift how far each lock's highlight is pushed along the strand so it breaks from lock to lock, and u_clumpGap how dark the seam between locks is; 0 leaves the card as authored. A hair material imported from a glTF is set to seven locks, so cards cut from one strand texture no longer read as a combed plate. Against Blender's render of the same frame the top of the hair comes out within about ten percent in every channel, where it was half again as bright.

  • Hair that reads as hair. The Hair shader has a lobe gain, u_lobeGain, for how much of the strand highlight a lamp draws apart from the reflection of the environment, and an imported hair material now sets it with the rest from what the source states: the specular factor as the environment's Fresnel, the roughness factor, a quarter of the lamp highlight, and a third of the shader's own strand shift and second highlight so the cards read as locks of hair and not plates. Against Blender's render of the same frame the hair comes out with a sheen on each lock and dark gaps between them, where it was a flat red plate.

  • Character materials shade like the source. A model imported with Character Materials now shades like the Principled shader it was authored against: a glTF carries no subsurface scattering and no second strand highlight, so the Skin shader's scattering, wrap and fuzz and the Hair shader's second highlight, translucency and root darkening are written as off, and a hair material's specular factor is scaled to the strand highlight's own units. Measured against Blender's render of the same frame, a character's hair comes out about 15 percent brighter than the reference where it was two to three times too bright, and the skin no longer glows in the shadows.

  • AgX view transform. A new post-process effect, agx, applies the AgX view transform Blender renders with, taken from the data Blender ships: scene-linear colour goes in and display-linear comes out, so put it last and leave the project's output transform at none. Its contrast setting is the look (1 is AgX Base, 1.2 is Medium High Contrast), it carries its own table, and against Blender's own render of the same frame the colours of a character come out within about two percent.

  • Hair maps from the model. A hair material's strand-direction map, highlight-shift map and tip softness are now imported from the model: a glTF has no slot for them, so a material names them in its extras (flow_map, shift_map, suggested_tipSoftness) and the importer binds them to the Hair shader instead of leaving the highlight to be worked out from the surface.

  • Spring-bone data in the file. The Spring Bones import setting now reads what a model states before it looks at names: the VRM spring-bone extension (VRMC_springBone, with its stiffness and gravity converted to units the engine uses) and a node's own extras.estella.springBone and springCollider properties, which carry exact sizes from a Blender custom property. Names fill in whatever those leave out, a chain with no collider list is kept out of every collider in the model, and springs that cannot be used are reported in the import warnings.

  • Spring-bone collider handles. A selected spring-bone collider carries six square handles in the viewport: on a sphere any of them sets the radius, on a capsule the four round its middle set the radius and the two on its ends set the height. The ones on the far side of the shape are faded, the value shows beside the pointer while one is dragged, a drag is one undo step, and with Preview FX on the strands follow it live.

  • Spring-bone import. A model's new Spring Bones import setting (By Bone Name) builds hair motion from names the file already carries: a bone called Hair__00 becomes a SpringBone with the bones below it as the strand, and an empty called SBC_Spherer or SBC_Capsule_r_h becomes a collider every chain is kept out of. The region sets a starting stiffness and drag, sizes follow the model's metres and the import scale, and the result is ordinary components to adjust in Details.

  • Spring-bone wind. A spring-bone chain takes a wind: wind is a steady acceleration on the chain, windGust swings its strength from calm to twice as strong, and windFrequency sets how often. Each bone meets a gust a moment after the one above, so a strand ripples rather than swaying as one, and a chain's gusts depend only on its own clock, so the same scene moves the same way twice. The wind is a field on each chain; there is no scene-wide wind source yet.

  • Spring-bone authoring. Spring bones can be authored in the editor: Details groups a chain's fields into Motion, Chain and Collision and counts the bones below it (or warns that there are none), the viewport draws a selected chain (the bones as posed, a ring at each joint, the tips) with the colliders it keeps out of, or a selected collider on its own, behind a Spring bones entry in the Gizmo menu, and View · Preview FX simulates the chain in the edit viewport, drawn solid over its dashed pose. Ending the preview gives the bones back as the scene has them.

  • Spring bones. Spring bones give a skeleton secondary motion: a SpringBone on the root of a chain of bones (hair, a tail, a cord) makes everything below it a spring that lags the animation, falls with gravity, is pulled back to its animated pose, never stretches, and is kept out of SpringBoneCollider spheres and capsules. It runs in C++ in the engine, so it costs the same on every target: 300 chains of six bones step in 0.28 ms. Only the bones' rotations are written, and switching it off gives the animation's own back.

  • Water rows in the quality settings. Project Settings → Rendering has a Level · Water group for the five water quality fields (waves, ripple cells, displacement, mirror, caustics) of the level chosen under Editing level. Each is saved as a difference to that level only, and the group is hidden while quality is off or the project excludes the water module.

  • Built-in shaders ship with their modules. The Water, Terrain, Foliage, Hair, Skin and Eye built-in shaders now ship with their modules instead of the core: a package that installs none of them no longer carries about 80 KB of shader source (a blank web package is 80 KB smaller, a blank WeChat bundle 7% smaller). A material that names one of them is what brings its module into an export, with no component in a scene needed; the three character shaders are a new esengine/character-shaders module.

  • Opaque models no longer vanish beside glass. Opaque draws are told from blended ones by the stage they are sorted in, not by their blend mode, when a frame has both a refracting surface and blended draws; before, a lit material whose blend mode was not None (the Model material) was treated as blended and could vanish from such a frame.

  • Water quality levels and a lite water. Water spends what the quality level allows. Each level's profile has waterWaves (waves summed, 2 / 3 / 4), waterRippleCells (the ripple grid's side, 32 / 64 / 128), waterDisplacement (off is the lite water: flat geometry with the waves in the shading only), waterMirror and waterCaustics. What Waters.sample answers follows what is drawn, and a minigame with no quality policy gets the Low level's water.

  • Move gizmo plane handles closer in. The three plane handles of the move gizmo sit closer to its centre, on the inside corner between each pair of axes, instead of out at a third of the way along them.

  • Water editing in the viewport. Select a lake or a river and its points are handles in the viewport: drag a point to move it on the body's own plane, drag the circle on an edge to insert one, press and release a point to delete it (a lake keeps three, a river two), and drag the handle beside a river's point to set its width there. Each gesture is one undo step. Details shows a water body only the fields its form uses, with a Boundary row for a lake's outline and the point count and the pressed point's width under a Spline. Also: the move gizmo now travels with the entity while an axis is dragged, instead of staying where the grab began.

  • Blended surfaces ordered against water. A blended surface (a pane, a particle, a leaf card) is now ordered against water and glass by the pixel and not by when it was drawn: one under the surface is seen through the water, one in front of it lies on the water instead of being covered, and where there is no water it is blended once. The frame is drawn in layers only when it holds both a refracting surface and a blended draw.

  • Ripples and wakes. A body of water keeps what disturbs it. With ripples above 0 the water steps a window of its surface (that follows the camera) with the wave equation: Waters.disturb(x, y, z, radius, strength) and a Floating body's wake (wake) put in bumps that spread as rings, fade, and bend the shading and the reflection, and leave foam that lingers and thins (rippleSpeed, rippleDamping, foamFade). Waters.sample carries the ripples, and Waters.rippleAt gives them alone.

  • Waterline, caustics, shafts and a mirror under the water. Under water the picture is lit and bounded like water. A camera with its lens half in the water has a waterline across it, soft and sliding as it dips; what it sees is lit by the web of caustics the waves focus the sun into (the Water material's u_causticStrength, u_causticScale and u_causticSpeed, also drawn on what lies under the water seen from above); the water along each ray scatters the sun into shafts (u_shaftStrength); and the surface seen from below, past the critical angle, mirrors the world beneath it.

  • Underwater view. A camera under water sees through it. Whenever the active camera is below a water body's surface an Underwater effect is added to the post-process volumes: the light of each pixel is taken out colour by colour (red first) by the distance it crossed, with the same absorption and density the surface's material draws with, and the water gives back its own colour, dimmer the deeper the camera is. The surface seen from below turns over and, past the critical angle, reflects whole (Snell's window).

  • Floating bodies. Water holds things up. A Floating component on an entity with a RigidBody3D floats it: a buoyant force against gravity (buoyancy is the water's density against the body's, so 2 floats it half under) and a drag against its velocity relative to the water, which brings it to rest in a lake and carries it with a river's current, applied at a few points of the body so waves tip it. It works from the CPU's own water surface, so it needs no renderer, and it needs the 3D physics module, which gains force and mass queries.

  • Rivers and splines. A WaterBody can be a river: put a Spline on its entity and it lays a ribbon along the curve, as wide as the curve says at each point and at its height, with a current (flowSpeed, faster where it narrows) that the shader carries the waves and ripples along and that Waters.sample reports as flow. Splines are a new optional module (esengine/spline): a curve through points with a width each, measured by the distance along it.

  • Lake outlines and shores. A lake can have an outline of its own and knows its shore. boundary cuts it to a polygon on its plane (crisp at the pixel, not at the grid's cells), shoreWidth calms its waves toward the edge and where it is shallow over a Terrain, and the shore foams along the outline. Waters.sample answers with the same calmed waves and says a point in the notch of a concave lake is not inside.

  • Water bodies. Water gets a component and a chapter of its own. A WaterBody builds an ocean (without an edge, following the camera, in rings that coarsen with distance) or a rectangular lake, lifts its geometry with the waves, follows its own up when it is tilted, and answers for its surface from code (Waters.sample) with the same formula the shader draws. Water is an optional module (esengine/water): a 2D game carries none of it.

  • Cyber Rooftop update. Cyber Rooftop gets a rebuilt rooftop model and textures, a wet roof material with a planar reflection under a rain sky, rain, fog and bloom in the scene, a larger set of HUD icons and inventory art, and a wider build footprint.

  • Hair tips. The Hair shader can draw the soft outer band of its cards: u_tipSoftness is how far below the cutoff alpha a second, blended pass draws each strand's tip fading out, sorted with the other blended draws, so tips are not stair-stepped by coverage. A model imported with Character Materials also has its hair triangles put in the order that pass wants, the cards nearest the middle of the hair first. Off by default.

  • Character materials on import. Importing a model can now give its character materials the right shader: turn on Character Materials in the model's import settings and a material whose name says skin, face, head, hair or eye gets the Skin, Hair or Eye shader with its normal and occlusion/roughness maps bound, and hair is cut out by its alpha and feathered by coverage. Off by default.

  • Curvature-shaped skin scattering. The Skin shader can take its wrap past the terminator per colour and shape it by curvature: u_preIntegrate (red travels furthest, every colour further where the surface turns sharply) and u_curvature, so a nose or an ear glows at its edge while a flat cheek stays crisp. Off by default.

  • Skin detail normal layer. The Skin shader has a detail normal layer: u_detailMap is a fine normal map tiled over the surface (u_detailTile) for pores and creases, laid on the sharp normal the shine reads by u_detailStrength (0 leaves it out) and not on the soft diffuse normals.

  • Cutout shadows for hair cards and leaves. A material whose alpha is coverage (u_alphaCutoff above 0, now also on the Hair shader) casts its cut-out shape into shadow maps instead of the rectangle it is drawn on, and Hair cards feather their edges with alpha-to-coverage.

  • Eye lid shadow. The Eye shader shades the eye the lids sit on: u_lidMap is an authored occlusion map, and u_lidShadow darkens, and fades the cornea's reflections, where the surface faces up from u_lidStart, which is the upper lid on an upright head.

  • Eye shader. A new Eye shader (builtin:eye) draws an eye from a mask and a dome normal map: the iris sits behind a refracting cornea so it moves as the eye turns, with a pupil, fibres and a limbal ring, a white lit with wrapped light, and a near-mirror cornea that shows every light as a catchlight.

Changed

  • Terrain is cut by its cell size. Its chunks, chunkCells and lodLevels are gone: the quadtree's levels follow from cellSize and the area (or drawDistance), and detail sets how far the finest cells reach.
  • WaterBody keeps only what a body says. Its material, cellSize, cells, rings, waveLength, waveSpeed, waveDirection and ripple fields have moved to the new WaterZone, which every body shares; shoreWidth now calms the waves toward the shore of an ocean and a river as well as a lake. A body of water is no longer tilted with its entity: it is turned about up only. ShoreField, buildRectMesh, buildBoxMesh and buildRiverMesh are gone from esengine/water.
  • A lite engine for mini-games. A WeChat or Douyin package whose scenes and scripts use no particle emitter, post-process volume or tilemap now ships a lite engine, the same engine without those three subsystems and about 200 KB smaller, so a UI-only game has room under the 4 MB main-package limit. It is chosen from the same evidence as the lean SDK entry plus a scan of the project's scripts; any mention of one of the three carries the full engine. Build it with node build-tools/cli.js build -t wechat-lite (-t all includes it).
  • Post-process effects are optional groups. The post-process effects' shaders are now three optional groups, esengine/fx-basic, esengine/fx-atmosphere and esengine/fx-lighting: a package carries a group when a PostProcessVolume names one of its effects (or a Weather or TimeOfDay component is in the content), and a game with no volumes ships none of them (about 180 KB less in a UI-only mini-game). An effect a script adds by code needs its group imported (esengine/fx-basic and the others) or the module set to Include; the editor and a non-lean entry carry all three.
  • Timelines are an optional module. Timelines are now an optional module, esengine/timeline: a package carries the timeline runtime and its loader when a scene has a TimelinePlayer, the content has an .estimeline, or a script imports the subpath, and a game with none of them ships neither. The API is unchanged and still reachable from esengine.
  • Terrain and Foliage are optional modules. Terrain and Foliage are now optional modules, esengine/terrain and esengine/foliage: a package carries them when a scene has a Terrain or Foliage component or a script imports the subpath, and a game with neither ships none of them (about 33 KB less in a UI-only mini-game). The API is unchanged and still reachable from esengine; the export installs the module for a scene that uses either component.

Fixed

  • Resizing a viewport no longer piles up render targets. A viewport dragged or docked through many sizes borrowed a full set of targets (the scene, its depth, the post chain) at each size and left each idle for two seconds before freeing it, which grew the GPU process to ten gigabytes in a few seconds and lost the graphics context. Targets nobody borrows are now kept within a budget of 256 MB, the longest idle first.
  • Many placed objects no longer crash the renderer. The texture that holds every object's record in a frame grew by doubling but was created from storage sized to the frame's need, so a frame with many objects read past its end and the engine aborted with memory access out of bounds; stopping then reported one warning for every entity. The texture is created from storage as tall as itself, and an aborted engine is reported once.
  • A cloud's shadow stays where the cloud is. The shadow a sky's clouds cast on the scene added the camera's position to the cloud's, so it slid across the ground as the camera moved while the cloud in the sky stood still; it is taken from the same place the sky draws the cloud from.
  • A leaf or a blade of grass no longer has a pale edge. A cutout texture (one with a Mip Coverage) has the colour of its solid texels carried into its clear ones when it is cooked, as a model's cutouts already did: what an artist leaves under alpha 0, often white, was averaged in at every leaf's edge by the filter and the mips. The source image is not touched.
  • A world's layers over an area are counted over the area. The chunks of a procedural layer are walked over the part of the wanted radius that lies in the layer's extent, not over the whole radius, and a generated world on an area of terrain takes the terrain's bounds as its extent: a layer wanted a thousand chunk-widths out no longer costs seconds a frame.
  • A level-of-detail group with only a cull size is no longer ignored. A LODGroup with no stand-in mesh and a cullSize draws the renderer's own mesh while it is big enough and nothing once it is smaller, where it used to be dropped as if it were not there, so a tuft of grass had no distance to be drawn to.
  • A river runs into a lake cleanly, and water casts no shadow. A river whose spline ran on into a lake drew a bright seam across the lake's bed: the surface was drawn into the sun's shadow map, darkening everything under it but where the river's heights changed its shape. Water no longer stands in the light's way (MeshRenderer.castShadows, new and on by default, says whether a mesh does), a lake now holds the water over a river that runs into it, and the surf's slope fades out on the ridge between two shores instead of flipping on a line.
  • Rain and storms on water. Rain no longer drops dips into the zone's ripples (which twitched and left blotches of foam under a storm): drops ring out on the surface in the Water shader, as fine rings that spread and die, the more the harder it rains, with no foam. The wind's ripples are six short waves around its heading instead of drifting noise, so a lake no longer shows cells of light and dark. A weather raises waves as far as the water is open to the wind (the sea whole, a lake by its width, a river hardly at all), and surf is swell from open water, so a lake's shore laps in a storm instead of breaking like the sea's; what floats rides the same water.
  • Shadows follow what the camera draws. A mesh the camera's level of detail culls (grass past its draw distance) no longer casts a shadow, and one drawn coarse casts coarse; seen from high up, a field of grass no longer laid an unseen square of shadow on the ground. The terrain's vertices slide between levels by the eye the ground was chosen for in every pass, so a shadow's ground is the ground that is seen, under an orthographic camera too.
  • Ridged ground without end has no steps. Infinite noise ground that was both ridged and worn broke into terraces with sheer walls where a ridge's fold turned; the crest is now rounded over a few hundredths, so the ground is continuous.
  • The running game's resolution can be chosen. A picker beside Play shows the target screen and stays usable while the game runs (it used to sit in a menu that was disabled in Play), with Free Aspect, the project's design resolution, the device presets and portrait or landscape; the choice is kept per project. (#100)
  • Terrain skirts and surf films no longer show as streaks. The skirt under a terrain chunk's edge, seen where two levels of detail part, took the relief bump meant for the ground and lit as a bright line; it now draws without it. The surf running up dry ground no longer stretches into a wall between two waters' heights.
  • Ripples and surf on calm water. The ripples a zone keeps fade out toward the edge of their window instead of stopping on a line across a lake, a breaker only whitens when it is steep for its length (a lake laps, the sea breaks), and foam seen from under the water is a faint shadow instead of a white sheet.
  • Water reads its data textures as numbers. The textures the water's field, shore and ripples are read from were decoded as sRGB colour, which bent their values.
  • A mesh on a material records its surface. An opaque mesh whose material has a blend mode of Normal (the default) was drawn blended, so it wrote no surface record, and the effects that read it (screen-space reflections, rain, contact shadows) passed over it, and alpha-to-coverage cut-outs on it had no coverage. It is now drawn opaque. Water and refracting glass write their own record where the device can blend one target and not another (WebGPU, and WebGL2 with OES_draw_buffers_indexed).
  • A terrain's splatmap keeps all four layers. A splatmap whose fourth layer is in its alpha no longer erases the other three where that layer is thin: it is read as the numbers in the file, not as a colour the browser premultiplies by its alpha.
  • Types for every SDK subpath. esengine/terrain, esengine/timeline, esengine/fx-basic, esengine/fx-atmosphere, esengine/fx-lighting and esengine/foliage named type declarations their package never built, and esengine/wechat built them without naming them, so a TypeScript project importing any of them got no types. All seven now ship and resolve, and the release check reads each entry's declarations where package.json says they are.
  • Dusk Fort exports. The Dusk Fort example's scene had no .meta, so an export of the project reached nothing from it: the package carried no assets and booted to "asset not found: assets/scenes/main.esscene". The scene now has its id, and the script that composes it writes one when it is missing.
  • Normal maps along a triangle's edge on WebGPU. On WebGPU with multisampling, every pixel a triangle only partly covers lost its normal map: on Metal the screen-space derivatives that build the frame were moved behind the branch that picks it, where those pixels have no neighbours to difference against. A normal-mapped quad drew a dotted line down its diagonal and round its rim, and the Skin shader's detail layer left those pixels without their highlight.
  • Cut-out colour in its clear texels. An imported cut-out's clear texels take the colour of the solid ones nearest them, so a mip, whether the cook makes it or the GPU does, no longer averages in what the painter left under alpha 0.
  • Cut-out shadows. A cut-out's shadow is the same shape on both backends: the shadow pass read a card's alpha at level 0 on WebGPU and through its mips on WebGL2. Both now read the mips and cut at the cutoff, as the cut-out itself is cut.
  • Imported eyes. An imported eye shades as the Principled shader it was authored against: the Eye shader's tinted white, its warm wrap of light past the terminator (which reddened a painted iris) and its lid darkening are written as off, and both its glossy layers take the source's roughness instead of a near-mirror cornea.
  • Energy in lit materials, and soft sun shadows as wide as the sun. Lit materials keep the light a rough surface reflects and pass on what it does not. The environment's specular came from Karis' analytic fit, 21% low for a metal at roughness 0.5 and up to 80% off at a grazing view; it is now a fit to the integral itself (tools/gen-dfg-fit.py, worst 3%). The light a rough metal loses to bounces between its microfacets is returned (Fdez-Aguera's multiple-scattering term, and the same ratio on a lamp's highlight), where a metal at roughness 0.9 under a white sky came out at half of Blender's brightness. The diffuse under a dielectric's specular is reduced by what the specular reflects, as in the Principled shader. And a lamp's highlight takes Smith's height-correlated masking at alpha = roughness squared in place of the Schlick k = (r + 1)^2 / 8 remap, which left a rough metal's rim a quarter dark. In the render lab six spheres under a white sky now match Blender within 0.5% at every roughness, and flat cards of five albedos within 0.002, on both backends. A sun with a size casts a penumbra as wide as Blender's. The disc a penumbra was averaged over stopped at 8 texels of the map, which held a sun's soft edge to sixteen texels: with a fine cascade that is less than the sun throws, and the render lab measured an edge 60% as wide as Blender's. The disc now reaches 24 texels, its spiral of taps is turned point by point so the wider disc grains rather than bands, and each blocker tap forgives only as far as it stands from the point, so a box standing on a floor stays shadowed to its foot.
  • Mip chains on WebGPU. On the WebGPU backend, a mesh without a material, a sprite and a particle are sampled through their texture's mip chain, as they always were on WebGL2: their WGSL read level 0 alone, so anything drawn smaller than its texture shimmered into stripes and moire there and not on the other backend. A compressed (KTX2) texture also keeps every level of its chain on WebGPU: the upload stopped at the first level whose side is not a multiple of four, which left a 1240x1268 hair atlas with level 0 alone. In the render lab a card of fine strands seen small now reads the same on both backends.
  • Cut-outs are glTF's MASK at every distance. A material with an alpha cutoff keeps a texel where its alpha reaches the cutoff and drops it where it does not, near or far, as glTF defines MASK; under multisampling alpha-to-coverage feathers that edge. Seen small, a cut-out keeps the area it covers because its mips do: the cook rescales each level's alpha so the share of texels past the cutoff is the full image's, now in both directions, so fine strands no longer close into a solid card when averaging lifts them over the cutoff, and sparse ones (a railing) still do not thin away. A soft painted band below the cutoff (eyelashes fading out) stays out at every distance. Khronos's AlphaBlendModeTest cuts each box at its own mark again. The shader helper is cutoutCoverage(alpha, cutoff, edge).
  • Faces speckled with dirt in shadow. The high quality profile's shadow cascades are now 1024 texels on a 4096 atlas (they were 512 on 2048). At 512 a cascade's texel is several millimetres across a close character, so a brow, a hairline or a lash card sitting a fraction of a millimetre above the skin fell inside one texel of it and the shadow speckled the face with dirt; at 1024 the face is clean. Medium and low are unchanged.
  • Ambient occlusion at metre scale. The ambient-occlusion effects, ssao and gtao, accept a radius down to a centimetre (they stopped at one world unit), so a 3D scene measured in metres can use them on hair and faces. A radius of 0.05 separates the cards of a hair mesh without the black speckle a larger one leaves on their edges.
  • Global illumination at metre scale. The global-illumination effects accept lengths down to a centimetre: the voxel size of voxelGI, and the radius and thickness of ssgi, used to stop at four, ten and one world units, so a 3D scene measured in metres could not set them small enough to do anything. Against Blender's render of the same frame, a character lit by one sun gets its soft bounce light in the shadows once they are set to metre-scale values.
  • Lite engine builds compile. The lite WeChat and mini-game engine builds compile again: the pass that puts the opaque scene back after water and glass named the post-process pipeline outside the guard that leaves it out of those builds, so every package without post-processing failed to build.
  • Shadow cascades fit the scene. A directional light's shadow cascades are now spread over the geometry that is in the scene instead of over the whole view. A close shot of a character in a view that reaches fifty metres used to spend its first cascade on three metres and drew blocky, misplaced shadow edges; the cascades now end where the scene's geometry ends (rounded in quarter octaves, so the split does not swim as the camera moves), and the shadow lands where it should.
  • Colour fringes on skin shadows. An imported skin no longer blurs its diffuse light separately per colour channel, which put a red and cyan fringe along the edge of every shadow on it; like the rest of the scattering, it is written as off unless the source says otherwise.
  • Opaque textures under a cutout material. A texture whose alpha is fully opaque, used by a cutout material (a hair material or any alpha-cutoff one), no longer vanishes once it is minified. Coverage-preserving mip levels were scaling its alpha down onto the cutoff, so every level past the first was discarded; a level that already reaches the share it must keep is now left alone.
  • Shadow program for a normal-mapped mesh with tangents. A model with tangents and a normal map no longer stops its shadow program from linking when the sun casts shadows. The depth pass wrote no tangent varying while the fragment stage still read one, so the shader failed on WebGL2 and the logged error named no material; the depth variant now leaves the tangent frame out.
  • Selection outline across a plane. The selection outline of a mesh no longer draws a stray line across a large plane seen from close by: a triangle with a corner behind the camera was judged by a different rule than its neighbour, so their shared diagonal showed as an edge.
  • Normal maps read against the frame they were baked in. Imported meshes now carry a tangent at each vertex (a glTF's own direction, or one made from the UVs), and normal maps on them are read against that frame instead of one derived per pixel, which was wrong at UV seams and on stretched textures. Re-import a model to get them; one imported before keeps its old shading. A mesh whose vertices carry a tangent is drawn with an extra vertex attribute and one more varying; a shader of your own that calls perturbNormal picks the frame up with no change.
  • New Project keeps the wrong template name. The New Project page no longer keeps the previous template's name when you pick another: the suggested name follows the template, and a name you typed stays.