Skip to content

Changelog: v2.14 ​

v2.14.0 (unreleased) ​

RaiSim

  • Explosions and projectiles (Explosions and Projectiles). World::addExplosion() sets off a TNT-equivalent charge whose blast wave spreads at the Rankine–Hugoniot shock speed and loads every dynamic body it reaches, panel by panel, with the incident, reflected and dynamic pressures of a Friedlander pulse fitted to Kinney and Graham's free-air curves; the load is integrated over each step exactly, so bodies gain the same momentum at any time step. Bodies hidden from the charge take only the wave that wraps around them, surface bursts load like heavier free-air charges, and craters lower heightmaps and deformable soil. World::addProjectile() flies shells, bullets and grenades under gravity and quadratic drag relative to the ambient air (whose velocity is the wind) or the liquid of a fluid volume (its sampled or wavy surface and current, where it has a sampler or waves), sweeps each step with a ray so fast rounds hit thin plates, hands the body it hits the impulse of a perfectly inelastic impact at that point (from its velocity relative to the point and the mass the body presents there, rotation and joints included; rounds that hit a body in the same step take turns), and, if explosive, detonates on impact or on a time fuse. World::getProjectileLaunches() lists recent launches, whatever became of them. Blasts, projectiles and the event lists are captured in checkpoints. The model is checked against Kingery–Bulmash values (IATG 01.80) by 37 new tests; the new explosion benchmark compares it with MuJoCo.
  • RayCollisionItem::getNormal() gives the surface normal at a ray hit.
  • Ray tests (rayTest(), lidars, projectiles and the blast's line of sight) are faster in worlds with many static bodies: the static bodies keep their BVH while none of them moves, and only the moving bodies are sorted into a tree again when bodies move. Results are bit-identical. A projectile volley over 1000 static bodies steps 34% faster; in the tank survival game, rebuilding the BVH took a sixth of each battle step and now takes 2%.
  • Mat's initializer-list constructor reports an oversized list out of line, so the constructor inlines without link-time optimization: RaiSim in the WebAssembly SDK steps as fast as with LTO (a contact-heavy scene went from 649 to 358 µs per step).
  • raisimPy binds explosions, projectiles and the blast model (raisimpy.blast.wave()).
  • raisimPy's rayrai module binds the explosion effects (ExplosionEffectSettings, ExplosionEffect, FireEffect, RayraiWindow.addExplosionEffect(), addFireEffect(), setExplosionEffectSettings(), clearExplosionEffects(), activeExplosionEffectCount(), prepareExplosionEffects()), the physics snapshot's explosions, projectiles, projectileHits and wind, and InstancedVisuals.setFoliageShadowMeshLodPixelError(). Its bindings had not been generated again after these rayrai headers changed, so the header inventory test failed.
  • .rscene import copies the values that hold no % escape as they are instead of byte by byte: the forest scene (2,425 objects) parses in 58 ms instead of 66 ms, and a world built from it loads about 10% faster, with the same scene.
  • Numbers in RaiSim's files (.rscene, world XML, URDF, MJCF) are converted with exact integer arithmetic (Clinger's fast path, then the Eisel–Lemire algorithm) when they have up to 19 significant digits and the default rounding mode is set, and with the C library otherwise. The result is the same double the C library gives, bit for bit. The forest .rscene (860,000 numbers) parses in 42 ms instead of 58 ms and a world built from it loads 22% faster; URDF robots load 4–5% faster.
  • URDF and XML files parse faster: the XML reader counts the columns of a run of plain characters at once when it records where each element starts, and gives the same line and column numbers in messages. Together with the faster numbers, ANYmal's URDF loads in 0.21 ms instead of 0.25 ms and Atlas's in 0.53 ms instead of 0.59 ms.
  • An articulated system whose state is set every step (a robot moved through recorded poses, an arm driven kinematically) keeps the storage of its contact Jacobians instead of allocating it again in the next step, and the internal contacts past getAllowedNumberOfInternalContactsBetweenTwoBodies() are marked without sorting them a second time. A Kinova arm on a table, set to a new pose every step, steps 7% faster, with the same results.
  • Fixed: an articulated system that had come to rest and fallen asleep ignored new PD targets and gains, so a parked robot stayed put however its joints were commanded until something else woke it. setPdTarget(), setPTarget(), setDTarget(), setPGains(), setDGains(), setPdGains() and setControlMode() wake it when they change a value, as a force does; setting the same values again lets it sleep.
  • The contact solver looks up last step's impulses (its warm start) in a flat open-addressing table instead of std::unordered_map, which divided by a prime on every lookup: primitive-heavy scenes step 1–3% faster. Simulation results are bit-identical.
  • A box, capsule or cylinder that hangs over the edge of a height map and touches nothing no longer tests all 16384 interior samples of the last recovery sweep every step. The samples are grouped into fixed spatial bins once, and a bin whose lowest point clears the terrain under the shape is skipped as a whole. The terrain case of the tracked-robot benchmark, where robots drive off the map edge, steps 7% faster. Simulation results are bit-identical.
  • Behaviour change: spherical joints have Coulomb friction (Joint Damping and Friction). The URDF <dynamics friction> of a spherical joint (and the PhysX jointFriction of a USD spherical joint) used to be ignored. It now bounds the magnitude of the joint's friction torque, the same in every direction: the joint is held at rest while the other torques on it stay below the bound, and otherwise braked with the bound against its relative angular velocity. A spinning joint slows down about a fixed axis, and a pendulum comes to rest anywhere inside its friction cone. The friction is solved with the contacts, the joint limits and the friction of the other joints; in a closed loop it acts only in the directions the loop lets the joint turn, and the loop's forces carry the rest. Robots without friction on spherical joints simulate as before, bit for bit. The new testJointFriction checks the step equation of every friction case against the model, and the new joint_friction benchmark times a 12-link chain of spherical joints and a humanoid ragdoll, and checks the chain's friction torques. The new example Spherical Joint Friction releases three spherical pendulums with no, a little and a lot of friction. Friction on the 12 spherical joints of a hanging chain adds 22 µs to its 3.4 µs step while they slide, and a humanoid ragdoll with friction on its 8 spherical and 4 revolute joints steps in 86–96 µs instead of 26 µs on the ground. With the default contact-solver settings, strongly coupled joints that mostly stick (the chain with 1 Nm of friction) leave some sliding joints braked by as little as 55% of the bound; with a tight solver tolerance they converge to the bound.
  • Fixed: RaiSim-sourced sensors drifted off their update grid. An IMU added its period to its next deadline and compared the sum with the world time. The rounding error of the sum grew with every sample, so about once per doubling of the simulated time one sample came a step late or a step early: a 500 Hz IMU at 2000 Hz slipped at 0.57 s, 2.3 s, 5.7 s and so on, or at 10.2 s, 19.4 s, 37.8 s and so on with the world time set to whole nanoseconds every step. A world clock that disagreed with the time step (whole nanoseconds at 3000 Hz) also slipped it a step every 333 s, and delayed a 2 ms latency to seven steps. Depth cameras and LiDARs were updated one period after their last update, rounded up to a step, so a 30 Hz camera at 1000 Hz ran at 29.4 Hz. All of them now take the deadlines of a fixed grid 1 / update_rate apart at the first step that reaches each one, counting physics time in time steps rather than world time (Update timing). The 30 Hz camera is updated 34, 33 and 33 steps apart in turn, 30 times a second, and the IMU every fourth step without slipping. IMU latency is counted in time steps too, and a latency just over a whole number of steps no longer drops to that number after a few minutes (2 ms and 1 ps at 1000 Hz took two steps instead of three after 140 s). The world time stamps the samples; a spinning LiDAR's head still turns by the world time between updates. Setting it back by more than a step still starts a new episode with an update at once. A new update rate now restarts the grid with an update at once for cameras and LiDARs, as it already did for IMUs. isUpdateDue() and setUpdateTimeStamp() still schedule sensors with a MANUAL source. detail::headerLayoutStamp() covers Sensor and InertialMeasurementUnit, whose layouts changed, so code compiled against older headers fails at World construction instead of misreading sensors. Counting the grid exactly costs an IMU 4 to 15 ns more per step (74 to 78 ns for an ideal IMU, 170 to 185 ns with noise, filters and latency; imu_sensor_benchmark).
  • Fixed: viewer-rendered MANUAL RGB/depth cameras retain their nominal sampling grid, so a 30 Hz camera at 1000 Hz averages 30 Hz with 33/34-step intervals instead of 29.4 Hz. Requests use compensated physics time for world and visual-only robots, and replies acknowledge the captured acquisition time. Delayed or duplicate replies retain the grid, and clock edits, rate/source changes and checkpoint restoration follow the rules in Update timing. New regressions cover the server round trip and the 3000 Hz nanosecond-correction case; manual_camera_benchmark checks RGB/depth cadence at 30, 300 and 500 Hz.
  • Segmentation cameras (Segmentation camera). raisim::SegmentationCamera reports what each pixel sees in up to three int32 images: the world index of the object, the body of that object and its semantic label, with -1 where nothing is hit. semantic, instance and panoptic cameras fill the label image, the object and body images, or all three. Object::setSegmentationLabel() labels an object, ArticulatedSystem::setBodySegmentationLabel() one body, and World::addSegmentationClass() names labels. URDF sensors of type="segmentation", world XML (segmentation_label, segmentation_class, <body_segmentation>, <segmentation_classes>), .rscene semanticClass keys, Engine 2 scenes and checkpoints carry the cameras, labels and class names. A MANUAL camera is rendered by rayrai or by a connected rayrai_tcp_viewer on the same sampling grid as RGB and depth cameras; with the RAISIM source, update() ray-casts the collision bodies. SegmentationCamera::colorize() colors an id image for display. raisimPy binds the camera, the labels and the class table, with images as (height, width) int32 arrays. The new segmentation_camera benchmark times the ray cast.
  • Fixed: changing a camera's image while a TCP viewer was connected (RGBCamera::setProperties(), a DepthCamera's properties followed by updateRayDirections(), or SegmentationCamera::setProperties()) left the viewer rendering the old image size or type; the server rejected its reply and dropped the connection. The server now sends the new properties before the viewer renders the camera again, for robots in the world and visual articulated systems alike. An image the viewer was already rendering when the properties changed is dropped without counting as a sample, so the camera is requested again with its new properties; a segmentation reply carries the segmentation type it was rendered with for this.
  • Fixed: the TCP viewer read a visual articulated system (addVisualArticulatedSystem()) with fewer than two visuals, such as a robot with one mesh or a camera rig without geometry, as a visual object and failed to parse the frame, and it rejected a frame whose first record was a visual articulated system. With the new protocol feature PROTOCOL_FEATURE_VISUAL_ARTICULATED_TYPE they arrive with an object type of their own (Protocol); viewers that do not negotiate it receive the old records. As with every new feature bit, a viewer of this release needs a server of this release: a server closes the connection of a viewer that sets a bit it does not know.

Rayrai

  • Rayrai draws the world's explosions and projectiles (Explosion effects): a ray-marched fireball that cools into billowing, sun-lit smoke drifting with the wind, sparks, debris and dust, a flash light and scorch decals, muzzle flashes, tracers and impact sparks. addExplosionEffect() draws an explosion that is not in the world, and setExplosionEffectSettings() tunes them. They follow physics snapshots and the world clock, so they pause and rewind with the simulation, and each effect is drawn from the event that caused it, so replays look the same. The flash light and scorch marks join the window's lights and decals only during a render, leaving the application's lists unchanged. prepareExplosionEffects() compiles their shaders ahead of the first explosion.
  • Explosions look photoreal in scenes lit by a strong sun: smoke, dust and debris are lit like the scene's meshes (the main light over pi). Ground bursts throw up a column of earth and dust under the smoke, dust that rolls out along the ground, and clods of earth that arc out and fall back; bursts at a water surface throw up spray and mist. ExplosionEffectSettings::smokeColor and dustColor color the world's explosions (soil, sand, snow or concrete dust), ExplosionEffect gains dustColor and inWater, and particles fade softly where they meet the scene.
  • Explosion fireballs and smoke are no longer round: each is a heap of uneven lobes laid out anew for every explosion, bent by low-frequency noise, with ragged, wispy rims eaten by finer noise, and the smoke heaps up as it rises. The sun's shadow inside the smoke takes two samples instead of three; an explosion-heavy frame costs about 0.4 ms more on an RTX 5070.
  • addFireEffect() starts a fire (FireEffect): flickering flames and a column of black smoke drifting with the wind, made of particles, for burning wrecks and fuel spills.
  • A rayrai muzzle flash is sized by the cube root of its projectile's kinetic energy: a machine gun's is a sixth of a tank gun's, so a burst no longer piles up into a white cloud. Shells of a megajoule and more look as before.
  • After prepareExplosionEffects(), foliage drawn with rayrai's sun-only programs also links the programs an explosion's light selects, so the first explosion no longer compiles three shaders mid-game (a 0.5 s stall in the browser); the image is unchanged.
  • Fixed: destroying an instanced batch whose LODs were still being prepared waited for the workers; in the browser, when they needed a new thread, the page froze for good (Tank Survival, choosing a level while the menu's models still loaded). The workers now finish on their own; on the desktop a window waits for them when it is destroyed.
  • InstancedVisuals::setFoliageShadowMeshLodPixelError() sets the mesh-LOD error budget of foliage in shadow maps apart from the visible one (setFoliageMeshLodPixelError() sets both until it is used), so trees can keep finer meshes on screen without paying for them in every shadow cascade.
  • Rayrai does less work for the same image: a river's bed is read back from the GPU only after it changes, the sky that water reflects is drawn into its cubemap again only when the sky changes (a generated weather sky included), and all explosion volumes of a frame take one instanced draw instead of one draw each. Shaders keep the locations of up to 128 uniform names (from 32), and instanced passes classify each foliage batch once. WaterDiagnostics::skyCaptures counts the sky captures. On the desktop, Tank Survival's levels render 7–13% faster (RTX 5070, 1600×900); simulation and images are unchanged.
  • Explosion smoke and dust fetch their noise only where it can change them (past the reach of a cloud's lobes, its dust column or the dust rolling along the ground, no noise brings smoke in), for the same image: looking into the dust of a dozen ground bursts, the volumes draw 22% faster on an RTX 5070 at 1600×900 (13.4 to 10.4 ms), and Tank Survival's dustiest frame (Frozen Ridge under a smoke screen and mortar fire) renders in 11.1 ms instead of 15.7. The new rayrai_explosion_volume_benchmark measures it.
  • An explosion effect added with addExplosionEffect() before a window's first frame is drawn: it starts by the world's clock, where it used to start by the wall clock and be dropped at the first render.
  • Explosion fire and smoke are much cheaper to draw and less grainy.
    • They are marched at half the image's width and height (ExplosionEffectSettings::volumeDownsample, 1 to 4, default 2) and blended into the image along its depth. Each marched pixel stops at the nearest scene depth it covers, and each image pixel blends the four by four nearest marched pixels whose depth matches its own, so smoke behind a nearer object does not spill over its edges or over objects thinner than a marched pixel.
    • Each ray takes about marchSteps steps across the volume, so rays that cross only its edge take fewer.
    • Neighbouring marched pixels start at interleaved fractions of a step, which the blend averages into smooth smoke instead of grain.
    • A cloud's lobes take one square root instead of seven, for the same image.
    • On an RTX 5070 at 1600×900, a smoke screen across the view adds 0.85 ms to the frame instead of 3.7 ms. Looking into the dust of a dozen ground bursts adds 1.8 ms instead of 10.4 ms, and 0.9 ms at a third of the resolution. rayrai_explosion_volume_benchmark reports both, and takes --downsample=N.
    • Tank Survival marches at a third of the resolution (half at Ultra). Its smoke screens and barrages no longer slow the game down on smaller GPUs and high-density screens, where the full-resolution march grew with the pixel count.
  • Fixed: explosions and fires could go undrawn where the window reuses what it drew before. An effect added with addExplosionEffect() or addFireEffect() to a scene in which nothing moved, or an explosion set off in such a world, showed the cached picture of the unchanged scene; behind a moving body, a scene whose static part comes from a cached layer drew no fire and smoke. While effects are in progress the window now draws every frame in full.
  • Fixed: a camera rendering a small static world with renderWithExternalCamera() and a fixed shadow center, moved away for one frame and back, kept showing the frame from where it had been. A cached frame is reused only while the camera has not moved since its last frame.
  • Fixed: under a low sun, surfaces in the far shadow cascades shadowed themselves: ground, rocks and vehicles went dark in patches that appeared and vanished as the camera moved (Tank Survival's Frozen Ridge, under a 17° sun). A far cascade's texels cover several times the ground of the first one's, and every cascade used the depth bias set for the first. Each farther cascade now moves its receivers along their normals by its filter footprint plus one texel, times how much wider its texels are than the first cascade's, and fits its map with that margin. Shadows stay attached to the objects that cast them (a larger depth bias would have pushed far shadows up to a metre away from their casters), and the first cascade is unchanged.
  • New example: rayrai_tank_survival, the Tank Survival game built from the same source as its web page, so the desktop and the browser play and look the same (the browser no longer plants different trees).
  • New example: Explosion and Projectile, the least code that uses projectiles and explosions: a gun fires a solid round and an explosive shell at a wall of crates, and a charge goes off beside it.
  • RayraiWindow::renderSegmentation() renders the object, body and label of every pixel into an integer target of the camera, drawing the geometry of renderDepthPlaneDistance() so segmentation and depth images of a camera agree pixel for pixel; Camera::getRawImage() fills a SegmentationCamera and Camera::readSegmentation() reads the images into buffers of your own. Visuals write the ids of Visuals::setSegmentationIds(). Fisheye depth and segmentation images are now resampled through the lens like color images, instead of being drawn with the pinhole projection. The TCP viewer renders the segmentation cameras of a server's robots, shows a colored preview and marks their frustums. raisimPy's rayrai module binds the segmentation renders and readbacks. The new example Segmentation Camera shows a color image and a segmentation image of one camera side by side.

Browser (WebAssembly)

  • The full webassembly-<version>.zip release now includes the SDK in sdk/, so the same download can run the demos and build custom dependent packages. Packaging requires and audits both parts before any upload. SDK files are frozen at audit time, private files and unexpected WASM sections are rejected, and built-in Rayrai shader implementation headers are omitted while the public Shader.hpp interface remains available. The standalone SDK archive remains available.

  • The WebAssembly SDK lets you build browser programs of your own on RaiSim and Rayrai. It holds prebuilt libraries, headers, the CMake packages raisim and rayrai, and a page runtime, for Emscripten 4.0.0. The libraries are WebAssembly objects linked into one object per library. They carry no source, no LLVM bitcode and no debug names, and a packaging audit enforces this. rayrai_add_web_app() turns an ordinary Rayrai program into a web page; raisim::raisim alone runs under Node.js. Releases publish webassembly-sdk-<version>.zip, and a local linux_install.sh unpacks it in raisim2Lib/wasm/sdk.

  • The WebAssembly license has a new clause 4 for programs built with the SDK:

    • Only academic users may distribute such programs, and only for academic purposes.
    • They distribute the program, never RaiSim or Rayrai themselves.
    • Every distribution must state clearly that the program is built with RaiSim and Rayrai under the license, and include it and the third-party notices.

    rayrai_add_web_app() and the new raisim_web_add_license() place the license and notices next to a program, and the generated page shows the notice.

  • rayrai_add_web_app() takes ASSETS (files and folders the page downloads into the program's file system before main(), each fetched and cached on its own, so a game's assets need not fit in one file), and DESCRIPTION, URL and IMAGE, which write the Open Graph and Twitter Card tags that make a shared link show as a card with a picture.

  • The page runtime of rayrai_add_web_app() downloads the program's WebAssembly and its assets together, with a progress bar, and keeps the shader programs its WebGPU backend compiles in the browser (Cache Storage, per build of the program): a second visit to Tank Survival reaches smooth play about 40% sooner. Assets that arrive after the program is instantiated go straight into its file system. The asset manifest holds the WebAssembly's size, so the bar is exact when the host compresses it, and a page whose sizes are unknown shows an indeterminate bar.

  • A page made by rayrai_add_web_app() writes its assets into the program's file system in one copy each instead of byte by byte (Emscripten's FS.writeFile): Tank Survival's page starts its program 1.6 s sooner on every visit (2.1 s to 0.5 s) and reaches a smooth menu 1.7 s sooner. The program exports _malloc, _free, _rayrai_web_write_file and wasmMemory for it.

  • Tank Survival's town is built from Poly Haven's modular apartment facades, each block weathered to a shade of its own and the burnt-out ones darkened with soot, and its fortress has crenellated walls and towers; rock shows on the steep hills around the arenas, and the sky over Frozen Ridge's snow is no longer darker than the ground; it is an example of its own in the docs (Tank Survival).

  • The WebGPU layer copies buffer readbacks (glMapBufferRange, glGetBufferSubData) from the mapped copy straight into WebAssembly memory, and makes each render target's view once instead of at every render pass that draws to it. Tank Survival draws its ground in 25 blocks instead of 100 chunks, each chunk keeping its own tangents, so the ground takes a quarter of the draws in every pass and shadow cascade with the same image. With the Rayrai changes above, Tank Survival runs 8–37% faster in Chrome (RTX 5070, 1600×900; Green Valley 105 → 127 fps, Iron Fortress 118 → 162 fps) with the same simulation and images.

  • Tank Survival is easier to play: the gun of the player's tank is stabilized (it keeps its aim while the hull pitches and rolls on slopes), and turrets and guns no longer jam against trees, which had kept them from turning to the cursor in the woods (the autopilot now hits 3 to 30 times as often). A dotted arc shows where a round fired now would fly and a ring where it would strike, red on a vehicle. A How to play button (or H) opens the controls, which also come up before the first mission. Tanks flatten the small trees of the arena, and shells and blasts fell them. Arrows at the edge of the screen point to enemies out of view, with their distance (and one beside the wind's speed shows where it blows), restarting takes a held R (E, the next weapon, is beside it), and a burst of hits on one spot shows one note with the damage summed.

  • Tank Survival stalls less in the browser: it compiles rayrai's core PBR program while loading (it was compiled the first time no high-fidelity mesh was in view, a 0.2 s stall mid-game), and before a level is first played it draws the level around the player behind the loading screen, with an explosion, until its models are in, so that the first explosion no longer stalls the game for 0.1 s while WebGPU makes its pipelines and the models are in place when play starts.

  • Fixed: a tank that drove into Ruined Town's canal could not climb out of it (its walls were three times as steep as a river's banks), which left the level stuck. The canal's banks are now as gentle as the rivers'; tank_survival_test checks that every river can be climbed out of.

  • Fixed: Tank Survival's Fast render quality was washed out (the preset shades without tone mapping, and the game's sun is lit for the tone-mapped PBR path). Fast keeps the tone-mapped PBR path with the lighter core shading: Green Valley takes 1.7 ms of GPU time per frame instead of 1.5 (Balanced 2.7) on an RTX 5070 at 1600×900.

  • Tank Survival in the browser keeps the best score of each level between visits, in the page's local storage.

  • Tank Survival's main menu lists its missions as cards, each with its number, name and setting on the left and its waves and best score on the right, in a panel that fits them.

  • Fixed: Tank Survival's frame rate dropped for a moment when the first vehicle of a kind was destroyed: its wreck built the levels of detail of its meshes then (10–20 ms natively, several times that in the browser), and in the browser WebGPU made the wreck's pipelines. Before a level is first played, the wrecks of every kind of vehicle it fields are drawn behind the loading screen, and one of each is kept.

  • Tank Survival is faster and more violent: vehicles drive 1.5 times as fast (the player's tank at 54 km/h), ground bursts throw clods of earth, sand, rubble or snow and stone that bounce and roll for a few seconds (rounds and the sight pass through them; a destroyed vehicle's own blasts throw none, as clods piled on a wreck kept the contact solver at its iteration limit), a destroyed vehicle's ammunition cooks off in five blasts and throws its turret high and spinning, fireballs are a third larger and nearby blasts shake the camera harder.

  • Fixed: Tank Survival's tanks barely turned while driving (W with A or D went on straight). The rigid hull bounced from bump to bump over the rough ground, airborne half the time at full speed, and its sprockets and idlers touched the ground at the far ends of the tracks, so the tracks had no grip to steer with. Each track now runs on a carrier sprung under the hull (it rises and pitches on springs), and the sprocket and idler ride above the road wheels as on a real tank: driving over rough ground, the player's tank turns at 12–60 degrees a second instead of 0–4, and every vehicle steers. A battle step takes about 30% longer. tank_survival_test drives each vehicle over rough ground and checks that it turns.

  • Tank Survival aims as a third-person shooter does. A click in the view captures the mouse (Esc frees it and pauses), which then looks over the tank's shoulder; every round lands where the crosshair in the middle of the view is, near or far: the game fires it on the path that reaches that point as the world flies it, with gravity, the air's drag and the wind. After a quick turn of the view, a click fires as soon as the turret has swung round to the crosshair. The crosshair turns red over an enemy, and a shot kicks the view up for a moment. Holding the right button aims down the sight, magnified three times (the wheel zooms from two to eight times), and the camera comes in front of walls, buildings, the ground and tree crowns between it and the tank. On a touch screen the sight stays where the finger taps and locks on the enemy nearest it. The light beams over supplies and the rings marking incoming strikes no longer cast shadows, which under a low sun lay across the ground as long dark lines and patches. tank_survival_test checks that a round fired by the game passes within 2 cm of the point under the crosshair at 30 to 400 m in the wind, that the gun fires only once it can reach the crosshair, that recoil settles where the view was, and the mouse look and the sight's zoom.

  • Tank Survival's guns no longer wobble: the turret drive took its gains from a nominal inertia far below the turret's and gun's, so it overshot a quarter turn by up to 0.4 rad and swung back and forth for most of a second after every turn of the view. It is now critically damped from the turret's own inertia, which also swings the hull less and stopped the turret's reaction from rolling light vehicles over. Enemies drive at seven tenths of their top speed and turn no sharper at speed than 3 m/s² sideways: at full speed and full steer, the BMD-2 rolled over on rough ground in most runs. The scenes are darker and clearer, with dark shadows: the ambient light (the sky's image-based light and the sun's ambient term) is a fifth of what it was, the exposure is lower, the distance haze is a quarter as dense and the weather's haze a third, whose bright color had lifted even nearby shadows (the umbra of a building on Ruined Town went from 78 to 44 of 255, beside sunlit ground at 152). Hazy levels keep their haze color in the distance. tank_survival_test checks that the turret stops on its angle without overshooting and that every enemy, steered hard over rough ground, stays on its tracks.

  • Tank Survival picks a lower render quality on a slow computer: while the main menu's level draws at under 30 frames a second, the menu steps the quality down and says so, until the player picks one. The game keeps real time down to 25 frames a second (it slowed down below 42).

  • Tank Survival shows a loading screen with the stage it is in, draws grass farther (60 to 170 m by render quality) and keeps finer tree and shrub meshes at a distance, and builds its levels a quarter faster with the same result.

  • Tank Survival is playable on the website, at raisim.com/games/tank-survival, and its rayrai example page and gallery tile have a Run on web button that opens it: the website lists the games it publishes in games/games.tsv, which the documentation reads like the browser tools' example lists. Documentation pages also describe themselves to the sites links to them are shared on: each has its own card title, description and address.

  • Fixed a browser crash ("Rayrai texture is unbound: additionalShadowAtlasMap"): shaders declare the shadow maps of additional lights even while none shines, and when the texture left on such a unit had been deleted, the WebGPU layer refused the draw. It now binds a neutral texture to them while the draw has no additional light, as it does for other maps a draw leaves unsampled.

  • New example project raisim2Lib/wasm, built with the SDK:

    • Tank Survival: six levels with their own terrain and weather, six kinds of enemy vehicle in waves, and five weapons (armor-piercing and high-explosive shells, a machine gun, guided missiles and artillery), with hits resolved against front, side, rear and roof armor. Seven armored vehicles from Sketchfab (CC BY 4.0) fight over rough terrain with photoreal ground, foliage, woods and hills to the horizon, a river they wade through, and burning wrecks.
    • A physics-only crate blast that runs under Node.js.

    The same sources build for the desktop.

  • Camera and sensor readbacks are much cheaper: readbacks wait once for the GPU instead of twice, readbacks in the stored format are plain copies instead of per-channel conversions, glReadPixels into client memory copies the mapped rows straight into WebAssembly memory, and pixel-pack copies with 256-byte rows stay on the GPU. Buffers created for reading (GL_*_READ) keep a mappable copy of each submission that writes them, so fenced or ring readbacks no longer stall the frame. While a readback waits, the browser layer asks the GPU queue for completion every 0.2 ms, because Chrome otherwise notices finished work only every few milliseconds (a 1 MB readback takes about 1 ms instead of 3). BGRA camera images upload one 32-bit word per texel. On an RTX 5070 at 1280×720 the RGB and depth camera examples run at 403 and 246 fps (from 72 and 64), the complete showcase at 159 fps (from 28), and the water stream holds 94 fps (from 42, with 47 ms spikes).

  • The browser's shaders keep the uniforms Rayrai sets for each object (its transform, color and material) in a uniform block of their own, so a draw copies about 1 KB instead of the whole 2–8 KB default block: the city uploads half as many uniform bytes per frame. With the draw-recording changes below, the city runs at 101 fps (from 94) and the off-road scene at 130 fps (from 108).

  • Draw recording in the WebGPU layer reuses pipeline keys, vertex bindings and resolved texture bindings while the state they depend on is unchanged, writes scalar uniforms without temporary arrays, and binds uniform groups without per-draw allocation: about 10–15% less frame time in draw-heavy scenes such as the city and off-road examples.

  • Browsers whose GPU samples 32 textures per shader stage now run Rayrai's desktop shader profile instead of the 16-unit one: planar ground reflections, height and parallax maps, subsurface maps and the full projected-decal pool. Textures with identical samplers share WebGPU's 16 sampler slots through a per-program assignment, so materials converge on one shader variant. Vertex inputs no bound array supplies read GL's generic attribute value, and every element of a sampler array names a texture unit.

  • The browser names its GPU adapter in the OpenGL renderer string, so the TCP viewer's automatic render quality matches the native choice for the same GPU (High instead of Balanced on discrete GPUs).

  • Touch input: the examples and TCP viewer take one finger as the left mouse button and a pinch as the wheel, and the scene editor adds pinch zoom and two-finger orbit; a second finger never clicks or leaves a drag pressed. Backing stores are capped at twice the CSS size on high-density phone screens, and canvases follow the visible viewport.

RaiSim — physics for robotics and AI.