Mechanic · environment · flight simulation
Thermal
A column of rising air that lifts an unpowered craft, so a glider can climb by circling in it.
On this page
A thermal is rising air that lifts a craft flying through it. In Pilotwings 64 each one is a cylinder whose updraft is strongest on its axis and whose swirl is fastest halfway out1.
- ColumnPilotwings 64 (1996). Each thermal is a cylinder of rising, swirling air, strongest on its axis, which the hang glider scales by 1.2 for Lark and Kiwi, 1.0 for Goose and Ibis, and 0.8 for Hawk and Robin.1,2
Games
| Game | Year | How | Note |
|---|---|---|---|
| Pilotwings 64 | 1996 | Column | each thermal is a cylinder of rising, swirling air, strongest on its axis, which the hang glider scales by 1.2 for Lark and Kiwi, 1.0 for Goose and Ibis, and 0.8 for Hawk and Robin |
Every claim about this mechanic, by game (2)
References
- ^ abIn the US version of Pilotwings 64, a thermal is a vertical cylinder from the task's data (
TaskTHER): a base position, a radius (scale), and a height.therm_80346C08sums a vector from each thermal whose cylinder holds the point (by the atlas's reading, its air velocity): its horizontal distance d from the axis at most the radius r, and its height z above the base between 0 and the thermal's height. Intherm_80346E04the vector's vertical part (the atlas's updraft) is the thermal'sunk24× √((r − d)/r), full on the axis and 0 at the edge. Beyond 1 unit from the axis its horizontal part is at right angles to the axis direction, of lengthunk20× (1 − 2|r/2 − d|/r) × d, longest halfway out (the atlas's swirl). Both fade in from 0 at the base to full at heightunk18, and fromunk1Cfade out to 0 at the top.env_802E1754adds the sum to the vectorwind_8034D550gives for the point (by the atlas's reading, the wind), along with, unless the task'sunk8is set, a further wind and a push from beyond 2050 units outside the map's bounds. The hang glider (hangGlider_802F391C) multiplies the total by its pilot'sunk2F8: 1.2 for Lark and Kiwi, 1.0 for Goose and Ibis, and 0.8 for Hawk and Robin.Coden64decomp/pw64revisedHow we know
274typedef struct { 275 Vec3F pos; 276 f32 scale; 277 f32 height; ⋮ 283} TaskTHER; // size = 0x28
src/app/task.h lines 274–283 at cb66cb2 · upstream ↗ 156void therm_80346C08(f32 x, f32 y, f32 z, Vec3F* dst) { ⋮ 174 continue; ⋮ 177 dx = x - therm->pos.x; 178 dy = y - therm->pos.y; 179 dz = z - therm->pos.z; 180 dist = uvLength2D(dx, dy); ⋮ 182 if (taskTherm->scale < dist) { 183 continue; ⋮ 185 if (taskTherm->height < dz) { 186 continue; ⋮ 188 if (dz < 0.0f) { 189 continue; ⋮
src/app/thermals.c lines 156–199 at cb66cb2 · upstream ↗ 206 temp_fv0 = therm->scale * 0.5f; 207 if (dist > 1.0f) { 208 if (dist < temp_fv0) { 209 var_fa0 = temp_fv0 - dist; 210 } else { 211 var_fa0 = -(temp_fv0 - dist); ⋮ 213 var_fv1 = 1.0f - ((2.0f * var_fa0) / therm->scale); 214 dst->x = therm->unk20 * var_fv1 * dist * (y / dist); 215 dst->y = -var_fv1 * therm->unk20 * dist * (x / dist); 216 } else { ⋮ 219 var_fv1 = uvSqrtF((therm->scale - dist) / therm->scale); ⋮ 221 var_fv1 = 1.0f; ⋮
src/app/thermals.c lines 201–235 at cb66cb2 · upstream ↗ 338void env_802E1754(f32 arg0, f32 arg1, f32 arg2, Vec3F* arg3) { ⋮ 347 wind_8034D550(arg0, arg1, arg2, &sp44); 348 therm_80346C08(arg0, arg1, arg2, &sp38); ⋮ 355 } else { 356 temp_fv0 = (temp_v0->unk0.unk0 - 2050.0f) - arg0; 357 if (temp_fv0 > 0.0f) { 358 sp2C = (50.0f * temp_fv0) / 2050.0f; ⋮ 361 if (temp_fv0 > 0.0f) { ⋮ 365 if (temp_fv0 > 0.0f) { ⋮ 369 if (temp_fv0 > 0.0f) { ⋮ 374 if (D_80362690->unkC[D_80362690->unk9C].unk8 == 0) { 375 arg3->x = sp44.x + sp38.x + D_80359D80.unk0.x + sp2C; ⋮
src/app/environment.c lines 338–382 at cb66cb2 · upstream ↗ 574STATIC_FUNC void hangGlider_802F391C(HangGliderData* hgData) { ⋮ 578 if (hgData->unk8C != 2) { 579 env_802E1754(hgData->unk10.m[3][0], hgData->unk10.m[3][1], hgData->unk10.m[3][2], &sp20); 580 sp20.x *= hgData->unk2F8;
src/app/hang_glider2.c lines 574–591 at cb66cb2 · upstream ↗ 519void hangGliderLoadPilot(u8 pilot, HangGliderData* hgData) { 520 switch (pilot) { 521 case PILOT_LARK: ⋮ 569 hgData->unk2F8 = 1.2f;
src/app/hang_glider1.c lines 519–569 at cb66cb2 · upstream ↗ 572 case PILOT_GOOSE: ⋮ 618 hgData->unk2F8 = 1.0f;
src/app/hang_glider1.c lines 572–618 at cb66cb2 · upstream ↗ 623 case PILOT_HAWK: ⋮ 669 hgData->unk2F8 = 0.8f;
src/app/hang_glider1.c lines 623–669 at cb66cb2 · upstream ↗ 674 case PILOT_KIWI: ⋮ 722 hgData->unk2F8 = 1.2f;
src/app/hang_glider1.c lines 674–722 at cb66cb2 · upstream ↗ 725 case PILOT_IBIS: ⋮ 771 hgData->unk2F8 = 1.0f;
src/app/hang_glider1.c lines 725–771 at cb66cb2 · upstream ↗ 776 case PILOT_ROBIN: ⋮ 822 hgData->unk2F8 = 0.8f;
src/app/hang_glider1.c lines 776–822 at cb66cb2 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^In the US version of Pilotwings 64, the thermals switch off in one test: the hang glider's test with index 0 in the Pilot class (
cls3,CLASS_PILOT). There, from 240 on the clockD_8034F850(by the decompilers' comment, the current time; the test's setup,gameUpdateStateTestSetup, sets it to 0, and each timefunc_80313D74runs it adds the time since the last frame held between 0.01 and 0.1, so it counts seconds of game time),therm_8034695Cnarrows the drawn cylinders to nothing over the next 10, and at 250 (THERM_DISABLE_TIME) removes the thermals and showsTEXT_THERM_OFF. The narrowing scales only the model's matrix: the updraft reads the task's radius until the thermals are gone.Coden64decomp/pw64revisedHow we know
14#define THERM_ENABLE_DURATION (4 * 60) // 4 minutes in sec 15#define THERM_WEAKEN_DURATION (10) 16#define THERM_DISABLE_TIME (THERM_ENABLE_DURATION + THERM_WEAKEN_DURATION)
src/app/thermals.c lines 14–18 at cb66cb2 · upstream ↗ 98 sThermShouldDisable = 0; 99 temp_v1_2 = &D_80362690->unkC[D_80362690->unk9C]; 100 if ((temp_v1_2->veh == VEHICLE_HANG_GLIDER) && (temp_v1_2->cls == 3) && (temp_v1_2->test == 0)) { 101 sThermShouldDisable = 1;
src/app/thermals.c lines 98–101 at cb66cb2 · upstream ↗ 53 CLASS_BEGINNER = 0, ⋮ 56 CLASS_PILOT = 3,
src/app/task.h lines 53–56 at cb66cb2 · upstream ↗ 114 var_fs0 = 1.0f; ⋮ 116 if ((D_8034F850 >= THERM_DISABLE_TIME) && (gThermalCount != 0) && sThermShouldDisable) { 117 thermDeinit(); 118 gThermalCount = 0; 119 gThermReady = 0; 120 hudWarningText(TEXT_THERM_OFF, 1.5f, 8.0f); ⋮ 127 if ((D_8034F850 >= THERM_ENABLE_DURATION) && (gThermalCount != 0) && sThermShouldDisable) { 128 var_fs0 = (f32)(1.0 - (f64)((D_8034F850 - THERM_ENABLE_DURATION) / THERM_WEAKEN_DURATION)); ⋮ 132 sp74.m[0][0] = taskTherm->scale * var_fs0; 133 sp74.m[1][1] = taskTherm->scale * var_fs0; ⋮ 140 uvDobjPosm(therm->unk0, 0, &sp74);
src/app/thermals.c lines 114–140 at cb66cb2 · upstream ↗ 182 if (taskTherm->scale < dist) { 183 continue;
src/app/thermals.c lines 182–184 at cb66cb2 · upstream ↗ 8f32 D_8034F850 = 0.0f; // current timesrc/app/code_9A960.c line 8 at cb66cb2 · upstream ↗ 210void func_80313D74(void) { ⋮ 213 temp_fv0 = uvGfxGetFrameTime(); 214 if (temp_fv0 < 0.01f) { 215 D_8034F854 = 0.01f; 216 } else if (temp_fv0 > 0.1f) { 217 D_8034F854 = 0.1f; ⋮ 221 D_8034F850 += D_8034F854;
src/app/code_9A960.c lines 210–221 at cb66cb2 · upstream ↗ 431s32 gameUpdateStateTestSetup(Unk80362690* arg0) { ⋮ 537 func_80313E0C(0.0f); ⋮ 545 return GAME_STATE_TEST_UPDATE;
src/app/game.c lines 431–545 at cb66cb2 · upstream ↗ 225void func_80313E0C(f32 arg0) { 226 D_8034F850 = arg0;
src/app/code_9A960.c lines 225–227 at cb66cb2 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
Sources
Code
- Smith, Garrett and pw64 contributors. 2026. “pw64: a decompilation of Pilotwings 64 (n64decomp/pw64).” Commit cb66cb2. n64decomp/pw64 @ cb66cb2 · licence, all uses