File size: 3,341 Bytes
3865888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
"""First-person geometric perception (V6 phase_07).

The organism NEVER receives hidden world state. Each observation is built
from MuJoCo raycasts through the live physics (occlusion included):

  eye position + yaw -> ray fan -> hits -> entities + depth panorama

Perception kind is honestly labelled GEOMETRIC (VLM slot: UNAVAILABLE until
a verified local vision model exists). Events fire on: new entity, close
obstacle ahead, novel location cell.
"""
import math
from typing import Any, Dict, List, Optional, Set

import numpy as np

from src.world3d.physics import EYE_HEIGHT

FAN_YAW = [-60, -40, -20, 0, 20, 40, 60]          # degrees, 7 horizontal rays
FAN_PITCH = [-10, 0, 8]                            # degrees, 3 rows
MAX_RANGE = 15.0


def _dir_vec(yaw_rad: float, pitch_rad: float) -> List[float]:
    return [math.sin(yaw_rad) * math.cos(pitch_rad),
            math.cos(yaw_rad) * math.cos(pitch_rad),
            math.sin(pitch_rad)]


class EyeSensor:
    def __init__(self):
        self.seen_entities: Set[str] = set()
        self.seen_cells: Set[str] = set()

    def observe(self, world, char_name: str, yaw: float) -> Dict[str, Any]:
        st = world.physics.char_state(char_name)
        eye = [st["pos"][0], st["pos"][1], st["pos"][2] + 0.55]
        depths: List[float] = []
        entities: Dict[str, Dict[str, Any]] = {}
        for pitch in FAN_PITCH:
            for dyaw in FAN_YAW:
                d = _dir_vec(yaw + math.radians(dyaw), math.radians(pitch))
                hit = world.physics.raycast(eye, d, MAX_RANGE, exclude_body=char_name)
                depths.append(hit["dist"] if hit["hit"] else MAX_RANGE)
                if hit["hit"]:
                    ent = hit["entity"]
                    prev = entities.get(ent, {"dist": 1e9, "count": 0})
                    prev["count"] += 1
                    prev["dist"] = min(prev["dist"], hit["dist"])
                    prev["geom"] = hit["geom"]
                    entities[ent] = prev
        events: List[Dict[str, Any]] = []
        for ent, info in entities.items():
            if ent not in self.seen_entities:
                self.seen_entities.add(ent)
                events.append({"type": "new_entity", "entity": ent,
                               "dist": info["dist"]})
        # close obstacle straight ahead (center column rays)
        center = [depths[r * len(FAN_YAW) + 3] for r in range(len(FAN_PITCH))]
        if min(center) < 1.5:
            events.append({"type": "obstacle_ahead", "dist": round(min(center), 2)})
        cell = f"{int(eye[0])}:{int(eye[1])}"
        if cell not in self.seen_cells:
            self.seen_cells.add(cell)
            events.append({"type": "novel_location", "cell": cell})
        pano = np.array(depths, dtype=np.float32)
        import hashlib
        frame_hash = hashlib.sha256(pano.tobytes()).hexdigest()
        return {"kind": "GEOMETRIC", "tick": world.tick,
                "eye": [round(v, 3) for v in eye], "yaw": round(float(yaw), 3),
                "depth_panorama": [round(float(v), 2) for v in depths],
                "entities": {k: {"dist": v["dist"], "count": v["count"],
                                 "geom": v.get("geom", "")}
                             for k, v in sorted(entities.items())},
                "frame_hash": frame_hash, "events": events}