On this page
  1. Related
  2. Games
  3. References
  4. Sources

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

GameYearHowNote
Pilotwings 641996Columneach 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)
Pilotwings 64 1996In 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_80346C08 sums 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. In therm_80346E04 the vector's vertical part (the atlas's updraft) is the thermal's unk24 × √((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 length unk20 × (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 height unk18, and from unk1C fade out to 0 at the top. env_802E1754 adds the sum to the vector wind_8034D550 gives for the point (by the atlas's reading, the wind), along with, unless the task's unk8 is 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's unk2F8: 1.2 for Lark and Kiwi, 1.0 for Goose and Ibis, and 0.8 for Hawk and Robin.Code
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 (cls 3, CLASS_PILOT). There, from 240 on the clock D_8034F850 (by the decompilers' comment, the current time; the test's setup, gameUpdateStateTestSetup, sets it to 0, and each time func_80313D74 runs it adds the time since the last frame held between 0.01 and 0.1, so it counts seconds of game time), therm_8034695C narrows the drawn cylinders to nothing over the next 10, and at 250 (THERM_DISABLE_TIME) removes the thermals and shows TEXT_THERM_OFF. The narrowing scales only the model's matrix: the updraft reads the task's radius until the thermals are gone.Code

References

  1. ^ 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_80346C08 sums 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. In therm_80346E04 the vector's vertical part (the atlas's updraft) is the thermal's unk24 × √((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 length unk20 × (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 height unk18, and from unk1C fade out to 0 at the top. env_802E1754 adds the sum to the vector wind_8034D550 gives for the point (by the atlas's reading, the wind), along with, unless the task's unk8 is 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's unk2F8: 1.2 for Lark and Kiwi, 1.0 for Goose and Ibis, and 0.8 for Hawk and Robin.Coden64decomp/pw64revised
    How 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 claim ledger

  2. ^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 (cls 3, CLASS_PILOT). There, from 240 on the clock D_8034F850 (by the decompilers' comment, the current time; the test's setup, gameUpdateStateTestSetup, sets it to 0, and each time func_80313D74 runs it adds the time since the last frame held between 0.01 and 0.1, so it counts seconds of game time), therm_8034695C narrows the drawn cylinders to nothing over the next 10, and at 250 (THERM_DISABLE_TIME) removes the thermals and shows TEXT_THERM_OFF. The narrowing scales only the model's matrix: the updraft reads the task's radius until the thermals are gone.Coden64decomp/pw64revised
    How 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 time
    
    src/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.

    In the claim ledger

Sources

Code