見出し画像

制空権。これが世界を動かしている



追尾する計算は、驚くほど簡単だ

空を飛ぶ飛行機を追いかける。
カメラでロックオンする。
逃げるターゲットとのズレを測り続ける。
そして少しずつ修正していく。

その様子を見ると、多くの人はこう思う。

「すごい計算をしているに違いない」

だが実際には、その本質は驚くほどシンプルだ。


本当にやっていること


追尾AIの基本は、実はこれだけである。

誤差 = ターゲット位置 - 自分の位置

つまり、

「どっち方向に、どれだけズレているか」

それを毎フレーム測っているだけだ。

例えば:

ターゲット x = 500
自分 x = 450

なら、

誤差 = +50

右へ50ズレている。

なら右へ動けばいい。

これだけだ。


しかし、これが世界を動かしている

驚くべきことに、この単純な構造は、

  • ドローン

  • ミサイル

  • 自動運転

  • カメラ追尾

  • ロボットアーム

  • AI制御

  • 工場機械

あらゆる場所に現れる。

つまり追尾とは、

誤差を減らし続ける運動

なのである。


未来を見ると、急に賢くなる

さらに少しだけ未来を予測すると、一気に「知能っぽさ」が出る。

例えば:

未来位置 = 現在位置 + 速度

を使う。

つまり、

「今ここにいる」ではなく、

「次にどこへ行くか」

を追う。

これだけで追尾は劇的に滑らかになる。


子供向けウェブアプリを作って気づいたこと

画面には、

  • ゆっくり逃げる飛行機

  • ロックオンカーソル

  • 誤差線

  • カメラ視点

  • スキャンHUD

が映る。

プレイヤーは何もしない。
ただ眺める。

しかし、見ているうちに分かってくる。

AIは魔法ではない。

巨大な意思があるわけでもない。

ただ、

ズレを見て
少し修正する

これを永遠に繰り返している。

それだけなのだ。


実はこれは「生物」に近い

考えてみると、人間も似ている。

歩くとき。

手を伸ばすとき。

ボールを取るとき。

全部、

目標との差

を修正している。

つまり知能とは、ある意味、

誤差修正システム

なのかもしれない。


そして計算は軽い

ここが面白い。

派手な見た目に反して、実際の計算量は非常に小さい。

毎フレームやっていることは:

  • 引き算

  • 足し算

  • 少しの速度更新

それだけ。

ESP32でも十分動く。

つまり、

知能っぽさ ≠ 巨大計算

なのである。


見える化すると、突然理解できる

数式だけでは分からない。

だが、

  • 誤差線

  • ロックオン円

  • 未来予測点

  • 滑らかな追尾

を画面で見ると、一気に直感へ変わる。

「ああ、こうやって追いかけているのか」

と理解できる。

これは非常に大きい。


追尾とは「世界への反応」

追尾AIは、世界を見ている。

そして、

ズレた
↓
修正する
↓
またズレる
↓
また修正する

を繰り返す。

この循環こそが、制御の核心だ。


たぶん、知能の原型もここにある

もしかすると。

知能の最初の形は、

難しい推論ではなく、

誤差を減らしたい

という運動だったのかもしれない。

追尾アニメーションをぼんやり眺めていると、そんな気がしてくるのである。



<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>追尾AI - LOCK ON DEMO</title>

<style>
html, body{
    margin:0;
    overflow:hidden;
    background:black;
    font-family:monospace;
    color:#00ff88;
}

canvas{
    display:block;
}

#ui{
    position:absolute;
    top:15px;
    left:15px;
    z-index:10;

    background:rgba(0,0,0,0.45);
    border:1px solid #00ff88;

    padding:14px;
    width:260px;
}

button{
    background:black;
    color:#00ff88;
    border:1px solid #00ff88;

    padding:10px 20px;
    cursor:pointer;

    font-size:16px;
    margin-bottom:10px;
}

#essay{
    position:absolute;
    right:15px;
    top:15px;

    width:320px;
    height:420px;

    overflow:auto;

    background:rgba(0,0,0,0.45);
    border:1px solid #00ff88;

    padding:14px;

    line-height:1.7;
    font-size:13px;
}
</style>
</head>
<body>

<div id="ui">

<button onclick="resetWorld()">▶ PLAY</button>

<div>
AI LOCK SYSTEM<br><br>

ERROR:
<span id="err">0</span><br>

LOCK:
<span id="lock">SEARCHING</span><br>

MODE:
AUTO PURSUIT<br><br>

TARGET SPEED:
<span id="ts">0</span><br>

CAMERA SPEED:
<span id="cs">0</span>
</div>

</div>

<div id="essay">

<b>追尾する計算は、驚くほど簡単だ。</b><br><br>

AIは魔法ではない。<br><br>

やっていることは、<br><br>

「ズレを見る」<br><br>

それだけである。<br><br>

ターゲットとの誤差を測り、
少しずつ修正する。<br><br>

その繰り返しによって、
追尾は成立する。<br><br>

未来位置を少し予測すると、
急に知能っぽく見える。<br><br>

だが実際の計算は軽い。<br><br>

引き算。
足し算。
少しの速度更新。<br><br>

それだけで、
「追いかける知能」が生まれる。<br><br>

このアニメーションは、
その最小構造を視覚化したものだ。

</div>

<canvas id="cv"></canvas>

<script>
const canvas = document.getElementById("cv");
const ctx = canvas.getContext("2d");

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const errText = document.getElementById("err");
const lockText = document.getElementById("lock");
const tsText = document.getElementById("ts");
const csText = document.getElementById("cs");

function rand(min,max){
    return Math.random()*(max-min)+min;
}

let stars = [];

for(let i=0;i<200;i++){

    stars.push({

        x:Math.random()*2000-1000,
        y:Math.random()*2000-1000,
        s:Math.random()*2
    });
}

let target;
let cam;

function resetWorld(){

    target = {

        x: rand(-300,300),
        y: rand(-200,200),

        vx: rand(-0.7,0.7),
        vy: rand(-0.5,0.5),

        timer:0
    };

    cam = {

        x:0,
        y:0,

        vx:0,
        vy:0
    };
}

function updateTarget(){

    target.timer++;

    // ゆっくり方向変化
    if(target.timer > 240){

        target.vx += rand(-0.4,0.4);
        target.vy += rand(-0.4,0.4);

        const sp = Math.hypot(target.vx,target.vy);

        const max = 1.5;

        if(sp > max){

            target.vx *= max/sp;
            target.vy *= max/sp;
        }

        target.timer = 0;
    }

    target.x += target.vx;
    target.y += target.vy;
}

function updateCamera(){

    // 少し未来を見る
    const futureX = target.x + target.vx*60;
    const futureY = target.y + target.vy*60;

    const dx = futureX - cam.x;
    const dy = futureY - cam.y;

    // 超ゆっくり誘導
    cam.vx += dx * 0.00018;
    cam.vy += dy * 0.00018;

    // 慣性
    cam.vx *= 0.985;
    cam.vy *= 0.985;

    cam.x += cam.vx;
    cam.y += cam.vy;

    const err = Math.hypot(dx,dy);

    errText.textContent = err.toFixed(1);

    if(err < 90){

        lockText.textContent = "LOCKED";
        lockText.style.color = "#00ff88";

    }else{

        lockText.textContent = "TRACKING";
        lockText.style.color = "yellow";
    }

    tsText.textContent =
        Math.hypot(target.vx,target.vy).toFixed(2);

    csText.textContent =
        Math.hypot(cam.vx,cam.vy).toFixed(2);
}

function drawBackground(){

    ctx.fillStyle = "black";
    ctx.fillRect(0,0,canvas.width,canvas.height);

    const cx = canvas.width/2;
    const cy = canvas.height/2;

    for(let s of stars){

        const px = cx + s.x - cam.x*0.2;
        const py = cy + s.y - cam.y*0.2;

        ctx.fillStyle = "rgba(255,255,255,0.25)";
        ctx.fillRect(px,py,s.s,s.s);
    }
}

function drawGrid(){

    ctx.strokeStyle = "rgba(0,255,100,0.06)";
    ctx.lineWidth = 1;

    const cx = canvas.width/2;
    const cy = canvas.height/2;

    for(let x=-2000;x<2000;x+=80){

        ctx.beginPath();

        ctx.moveTo(
            cx + x - cam.x,
            0
        );

        ctx.lineTo(
            cx + x - cam.x,
            canvas.height
        );

        ctx.stroke();
    }

    for(let y=-2000;y<2000;y+=80){

        ctx.beginPath();

        ctx.moveTo(
            0,
            cy + y - cam.y
        );

        ctx.lineTo(
            canvas.width,
            cy + y - cam.y
        );

        ctx.stroke();
    }
}

function drawTarget(){

    const cx = canvas.width/2;
    const cy = canvas.height/2;

    const x = cx + target.x - cam.x;
    const y = cy + target.y - cam.y;

    // 未来位置
    const fx = cx + (target.x + target.vx*60) - cam.x;
    const fy = cy + (target.y + target.vy*60) - cam.y;

    // 予測線
    ctx.strokeStyle = "lime";

    ctx.beginPath();
    ctx.moveTo(x,y);
    ctx.lineTo(fx,fy);
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(fx,fy,8,0,Math.PI*2);
    ctx.stroke();

    // ロック枠
    ctx.strokeStyle = "red";
    ctx.lineWidth = 2;

    ctx.strokeRect(x-35,y-35,70,70);

    // 飛行機
    ctx.save();

    ctx.translate(x,y);

    const ang = Math.atan2(target.vy,target.vx);

    ctx.rotate(ang);

    ctx.fillStyle = "orange";

    ctx.beginPath();

    ctx.moveTo(28,0);
    ctx.lineTo(-18,-10);
    ctx.lineTo(-6,0);
    ctx.lineTo(-18,10);

    ctx.closePath();
    ctx.fill();

    ctx.restore();
}

function drawHUD(){

    const cx = canvas.width/2;
    const cy = canvas.height/2;

    ctx.strokeStyle = "#00ff88";
    ctx.lineWidth = 2;

    // 中央ロック円
    ctx.beginPath();
    ctx.arc(cx,cy,90,0,Math.PI*2);
    ctx.stroke();

    // クロスヘア
    ctx.beginPath();

    ctx.moveTo(cx-140,cy);
    ctx.lineTo(cx-40,cy);

    ctx.moveTo(cx+40,cy);
    ctx.lineTo(cx+140,cy);

    ctx.moveTo(cx,cy-140);
    ctx.lineTo(cx,cy-40);

    ctx.moveTo(cx,cy+40);
    ctx.lineTo(cx,cy+140);

    ctx.stroke();

    // スキャン線
    const sy =
        (Math.sin(Date.now()*0.001)+1)*0.5
        * canvas.height;

    ctx.strokeStyle = "rgba(0,255,100,0.15)";

    ctx.beginPath();
    ctx.moveTo(0,sy);
    ctx.lineTo(canvas.width,sy);
    ctx.stroke();

    // LOCK表示
    const tx = cx + target.x - cam.x;
    const ty = cy + target.y - cam.y;

    const d = Math.hypot(tx-cx,ty-cy);

    if(d < 90){

        ctx.fillStyle = "#00ff88";
        ctx.font = "32px monospace";

        ctx.fillText("LOCK",cx-45,cy-120);
    }
}

function draw(){

    drawBackground();
    drawGrid();
    drawTarget();
    drawHUD();
}

function update(){

    updateTarget();
    updateCamera();
}

function loop(){

    update();
    draw();

    requestAnimationFrame(loop);
}

resetWorld();
loop();

window.addEventListener("resize",()=>{

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
});
</script>

</body>
</html>

いいなと思ったら応援しよう!