Skip to content

Sensing the world

Rangefinders, light sensors, line followers and cameras are all built from three general tools. None of them knows what device it serves.

Tool Where Answers Call it from
Ray: Cast and CastTo Any rendering node What is along a line: hit or not, distance, normal, angle, and the struck surface’s own fields PhysicsUpdate
Field: SampleEnvironment and SampleField Circuit logic (at the part), or any rendering node (at that node, facing a direction) A named value at a point, such as "Lux" or "TemperatureC", from environment zones and emitters PreStep (circuit), Update (component)
Eye: ImageSensorWrapper An image sensor marker A small rendered picture of what the node faces Update, one frame after Capture()

A field is what a sensor measures. An eye is what a camera photographs. A photoresistor reads the Lux field and doesn’t change with the viewport’s lighting toggle. A camera module sees the lit scene.

A ray reports the surface fields of the body it strikes: named numbers such as IrReflectance: 0.05 or AcousticReflectance: 0.1. The names are yours to choose. Your sensor reads one with hit.SampleSurface("IrReflectance", fallback), and gets the fallback when the surface doesn’t have that field, so keep a sensible default in a property.

Surface fields are part of a body’s settings. Select the body, and in the inspector’s Sensing section type them into Surface Fields, one per line, such as IrReflectance: 0.8.

  • For every copy of a part, set them in the component editor, on the main body under Physics or on a mesh’s part body. A black chassis can carry IrReflectance: 0.05 wherever it goes.
  • For one copy in a circuit, set them on the part’s Rigidbody row in the hierarchy. This is how you mark the bench: a flat prop with IrReflectance: 0.8 makes a white track, and thin props on it with IrReflectance: 0.05 make the line. An edit takes effect at once, even while the simulation plays.

The floor carries no fields, and neither does a part without a body. Rays only see bodies, which exist while the simulation plays, and a ray never strikes the part casting it.

// Ultrasonic ranger: the "Transducer" node looks along -Z. Centimetres to the circuit.
public override void PhysicsUpdate(double delta)
{
var hit = Rendering.Transducer.Cast(Vector3.Forward, 4.0);
Circuit.UserData[0] = hit.Hit ? hit.Distance * 100.0 : -1.0; // -1 = no echo
}
// Reflectance sensor: how much infrared the surface under the emitter sends back.
public override void PhysicsUpdate(double delta)
{
var hit = Rendering.Emitter.Cast(Vector3.Down, 0.05);
Circuit.UserData[0] = hit.SampleSurface("IrReflectance", Properties.Reflectance) * hit.Incidence;
}
// A lamp: a FieldEmitter node named "Emitter" with FieldName "Lux". Brightness follows current.
public override void Update(double delta)
{
Rendering.Emitter.Intensity = Properties.LuxPerAmpAt100mm * Circuit.GetCurrent(0);
}
// Optical line follower: an 8 x 1 image sensor named "Eye" looking down. Left and right halves to the circuit.
private int _seen;
public override void Update(double delta)
{
var eye = Rendering.Eye;
if (eye.FrameVersion != _seen)
{
_seen = eye.FrameVersion;
Circuit.UserData[0] = eye.MeanLuminance(0, 0, eye.Width / 2, eye.Height);
Circuit.UserData[1] = eye.MeanLuminance(eye.Width / 2, 0, eye.Width, eye.Height);
}
eye.Capture();
}

In circuit logic, fall back to the matching property when nothing is sensed, so the part still works on an empty bench: SampleEnvironment("Lux", Properties.Lux).

  • A ray is one query per call. Cast at the device’s own rate (an HC-SR04 pings every few milliseconds), not every step, especially for a cone of rays.
  • A capture is a small render plus a GPU readback, with at most 8 per frame across all sensors. Capture only when you need a picture.