Shopping Visual Search
Not in included Example Scripts, but see Download button below to get the latest version
Download Latest Version
This experiment simulates a visual search task within a virtual supermarket aisle. Participants are instructed to locate and grab a target item from a dense array of shelf-placed distractor items using gaze and controller-based interaction.
Overview
Participants are placed in a VR environment and presented with instructions to locate a specific food item (e.g., Breakfast Tuna). Items are randomly placed across a left and right shelf grid, with a single target placed randomly among them. One random seed draws the whole layout, so a session's layout can be repeated exactly.
When the target is found (grabbed), feedback is presented (image quad and sound), and the time taken to find it is recorded to the experiment summary, along with where the target was. The trial continues until the experimenter presses Space, or until an optional time limit runs out. A trial in which the target was never grabbed is recorded as not found.
Core Features
- Random placement of 3D food models on two opposing shelves, drawn from one seed that is saved with the data.
- Trial logic with instructions and fade-in/out transitions.
- Grab detection with optional visual feedback. Every grab and release of any item is recorded in the trial data.
- Timing from the start of the search to the first grab of the target, logged as
"Time to Find Target". - The target's side, column, shelf and position recorded in the experiment summary.
- Space ends the trial; an optional time limit can end it too.
- Image-based instructions head-locked to the participant's view.
- Optional audio and environment constraints.
- A multi-user version (server and client), described below.
Configurable Parameters
| Variable | Description | Default Value |
|---|---|---|
TARGET_OBJECT |
The internal name of the target item to find | "BreakfastTuna" |
TARGET_FILE |
The filename of the .osgb model for the target | "f_breakfastjerky_7A.osgb" |
SHOW_ITEM_INFO |
Whether to display a quad image above found object | True |
TIME_LIMIT |
Seconds the participant has to search. None: no limit, and only Space ends the trial |
None |
RANDOM_SEED |
Seed for the shelf layout. None draws a new seed for each session; a number repeats a session's layout exactly |
None |
left_origin / right_origin |
Grid positions for shelf start locations | Custom 3D coords |
cols, rows |
Grid layout dimensions for shelf item placement | 20 cols x 5 rows |
The seed is printed when the script starts and saved in the experiment summary's Random Seed column. To show the same layout again, set RANDOM_SEED to that number.
Components
ShelfGrid System
The ShelfGrid class (grid_placer.py) provides methods to position items in a grid with optional rotation and depth offset.
Modify:
- Grid size and orientation
- Sampling behavior (use
.sample_positions(n)if desired)
Scene Object Management
Each item is added to the scene with SightLab tracking. The target is a gaze object, so the usual dwell and fixation measures cover it, and every item is grabbable:
sightlab.addSceneObject(item_name, item, gaze=enable_gaze, grab=True)
The target is named BreakfastTuna (TARGET_OBJECT). The other items are named by shelf and slot: food_90_<n> on the left shelf and food_-90_<n> on the right. The names are the same in every session, so the data from different sessions can be compared item by item.
All added objects are also tracked in a scene_items list for batch visibility control:
for item in scene_items:
item.visible(viz.OFF) # Hide all items for example
Instructions Panel
Image-based instructions are locked to the participant's head using the following logic:
headRoot = viz.addGroup()
viz.link(mainViewNode, headRoot, mask=viz.LINK_ALL).preTrans([0, -2.0, 1.2])
sightlab.instructionsImageQuad.setParent(headRoot)
You can scale the quad to suit portrait-style instructions:
sightlab.instructionsImageQuad.setScale([1.2, 1.8, 1]) # Taller portrait
For landscape instructions, consider:
sightlab.instructionsImageQuad.setScale([1.8, 1.2, 1]) # Wider landscape
Grabbing and Timing
The search starts when the trial starts, after the instructions. The first time the participant grabs the target in a trial:
- ✅ Sound feedback is played.
- ✅ A floating image quad can be displayed over the item.
- ✅ The time from the start of the search to the grab is measured and saved to the experiment summary:
search['time'] = round(sightlab.hud.getCurrentTime() - search['start'], 3)
sightlab.setExperimentSummaryData("Target Found", True)
sightlab.setExperimentSummaryData("Time to Find Target", search['time'])
The trial does not end at the grab. It ends when the experimenter presses Space or, if TIME_LIMIT is set, when the time runs out.
The script's grab handler is registered once, before the trial loop, with vizact.addCallback:
vizact.addCallback(grabber.GRAB_EVENT, onGrab)
Use vizact.addCallback for your own grab handlers too, not viz.callback. viz.callback keeps one handler per event, so it replaces SightLab's own grab handler: SightLab then stops recording grabs and, in the multi-user version, stops sending grabbed items' movements to the server.
Data Recorded
Each trial's row for ENTIRE_SCENE in the experiment summary has these columns:
| Column | What it records |
|---|---|
Target Found |
True if the participant grabbed the target, False if the trial ended first |
Time to Find Target |
Seconds from the start of the search to the first grab of the target. Blank when the target wasn't found. |
Target Side |
left or right |
Target Column |
Which column of the shelf. Column 1 is 2.5 m in front of the starting point, and each next column is 0.22 m further back. |
Target Shelf |
1 (bottom) to 5 (top) |
Target Position |
The target's position (x, y, z) in meters |
Random Seed |
The seed that drew the layout (see RANDOM_SEED) |
Trial End |
space key or time limit |
The trial data also flags every grab and release, of any item, as grabbed: <name> and released: <name>, so a participant's wrong grabs can be counted from it.
Running the Experiment
To run the full experiment:
- Run
Main_Shopping_Search.py - Choose options such as if running BIOPAC, eye tracking thresholds and click
Continue - Choose hardware from the dropdown (note: this list can be modified)
- Enter Participant ID and click
Submit - Put on the headset, read the instructions and press the trigger to begin the task
- Look for the intended target, use the trigger buttons to grab items (target is identified when grabbed). On Desktop, the left mouse button grabs what the hand is touching.
- Additional controls here
- Press
Spacebarto end the trial, whether or not the item was found, and the data will be saved (might take a few moments). IfTIME_LIMITis set, the trial also ends when the time runs out. - Please wait until it says "Experiment Complete" for the replay to work
- Run
SightLabVR_Replayto see a replay of the session with visualizations - View the data in the
datafolder
Multi-User Version
Main_Shopping_Search_Server.py runs on the experimenter's machine, and Main_Shopping_Search_Client.py runs on each participant's machine.
- Same shelves everywhere. The server and every client build the shelves from
RANDOM_SEED, so they all show the same layout with the same item names. The seed must be the same number in both scripts (default1). For a different layout, change it in both. - Finding the target. Each client times its participant's search from the moment they close the instructions, so reading time is not included. When the participant grabs the target, the client sends that time to the server. The server records the first client to find it in
Found By, together withTarget FoundandTime to Find Target, and plays the feedback. - Ending the trial. Space on the server ends the trial.
TIME_LIMITis set in the server script. There it counts from the start of the trial, so it includes the time a participant spends on the instructions. - Grabbed items. A grabbed item's movement is sent to the server, so the experimenter sees it move.
Number of items: every frame, a client sends the server the position of every scene object in one network message, which must stay under about 64 KB. The default of 20 columns per side (200 items) fits. 25 columns (250 items) does not: the messages never arrive, and the server shows no head, hands or grabbed items for that client. If you add items or other scene objects to the multi-user version, keep the total under about 200.
Possible Modifications
-
Place new
.osgbmodel files inresources/foods/ -
Set
gui=Falsewhen initializingSightLab()if not using the GUI -
Configure
vizconnectfor your headset, controller, and eye-tracker -
Item Sampling
Replace:
grid.all_positions()
with:
grid.sample_positions(n)
to reduce the number of distractor items.
- ROI Tracking
Use the Scene Editor (Inspector) to placeRegionOfInterest.osgbobjects and register them with:
sightlab.addSceneObject("roi_name", roi_object, gaze=True)
- Audio Feedback
Replace sound files:
backgroundAudio = viz.addAudio('Resources/audio/your_custom_loop.wav')
foundAudio = viz.addAudio('Resources/audio/your_success_sound.wav')
- Dynamic Instructions
You can show multiple instruction slides:
yield sightlab.showInstructions(image='sightlab_resources/instruction1.jpg')
yield viztask.waitEvent('triggerPress')
yield sightlab.showInstructions(image='sightlab_resources/instruction2.jpg')
Assets Structure
resources/
├── audio/
│ ├── supermarket.wav
│ └── tada.wav
├── foods/
│ ├── f_breakfastjerky_7A.osgb
│ ├── ... (other distractor items)
└── images/
└── item_found.png
Notes
- Use
sightlab.getEnvironment().visible(viz.OFF)to hide the environment at startup. - Use optimized or low-poly
.osgbmodels for better performance. - Be sure to add all objects using
sightlab.addSceneObject(...)(withgaze=Trueand/orgrab=Trueas needed) to ensure proper data collection. - Give each scene object its own name: SightLab keeps one object per name for gaze data and replay.
Dependencies
sightlab_utilsviztask,vizconnect,vizproximity- Custom tools:
grid_placer.pytools/grabber.py
Authoring Tips
-
Use Inspector to place and align 3D models on shelves.
-
Adjust object orientation with
.getEuler()and.setEuler(). -
Enable proximity debug with:
manager.setDebug(viz.ON)
Additional Options
This experiment can be extended in a variety of ways depending on your research goals:
- Multi-user mode for collaborative or competitive tasks, building on the server and client scripts
- Biopac integration for synchronized physiological data collection
- Real-time biofeedback to visualize participant state (e.g., heart rate, EDA)
- Dynamic cutoffs that change the time limit (
TIME_LIMIT) per trial or condition - User surveys and self-report (ratings, open-text input)
- Custom event tracking (e.g., time in ROI, distance walked)
- Condition randomization or block-based designs
- Gaze zone and ROI analysis using tagged scene regions
- Environmental changes like lighting, sound, or object density per trial
- GUI integration for managing trial parameters, objects, and logging visually
These features can be enabled or combined depending on your experimental design needs