-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathIXR_VR_Controller.cs
417 lines (343 loc) · 15.4 KB
/
IXR_VR_Controller.cs
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LSL;
public class IXR_VR_Controller : MonoBehaviour
{
public static IXR_VR_Controller Instance;
[Header("BCI and simulation")]
public float focusLevel = 0.5f;
private float currentMovementFocus = 0.0f;
private float currentBCIFocus = 0.5f;
public bool doSimulateBCI = true;
public enum BCISimulationMethod
{
random, constant
}
public BCISimulationMethod BCISimulation;
public bool useMotionForSimulation = true;
private float BCISimulationIntervall = 0.1f;
private int pastMovementVectorsAmount = 240;
private float movementFocusScale = 0.2f;
[Header("HMD and hands for simuation")]
public GameObject HMD;
public GameObject leftHand;
public GameObject rightHand;
private Vector3 lastHMDpos;
private Quaternion lastHMDrot;
private Vector3 lastLeftHandPos;
private Quaternion lastLeftHandRot;
private Vector3 lastRightHandPos;
private Quaternion lastRightHandRot;
private float currentHMDposVel;
private float currentHMDrotVel;
private float[] pastHMDposVel;
private float[] pastHMDrotVel;
private float currentHMDposFocus = 0;
private float currentHMDrotFocus = 0;
private float meanHMDPosVel;
private float meanHMDRotVel;
private float currentleftHandposVel;
private float currentleftHandrotVel;
private float[] pastleftHandposVel;
private float[] pastleftHandrotVel;
private float currentleftHandposFocus = 0;
private float currentleftHandrotFocus = 0;
private float meanleftHandPosVel;
private float meanleftHandRotVel;
private float currentrightHandposVel;
private float currentrightHandrotVel;
private float[] pastrightHandposVel;
private float[] pastrightHandrotVel;
private float currentrightHandposFocus = 0;
private float currentrightHandrotFocus = 0;
private float meanrightHandPosVel;
private float meanrightHandRotVel;
private float lastBCISimulationChange = 0;
private float rotThreshold = 5;
private float posThreshold = 0.02f;
[Header("receiving data")]
public string streamName = "BrainPower"; //naming is essential here! otherwise the inlet can't be found
private ContinuousResolver resolver;
private StreamInlet inlet;
private float[] sample;
private float disconnectTimer;
private float disconnectTimeLimit = 1;
[Header("pushing event markers")]
public string senderStreamName = "IXREventMarker";
private string streamType = "Markers";
private static StreamOutlet outlet;
private static string[] sampleString = { "" };
private void Awake()
{
//as the VR BCI Controller should not be destroyed when loading another scene,
//and to avoid having multiple VR BCI Controllers in one scene, a Singleton pattern will
//make it easier to always have only one (and the same) Instance in a scene, while also
//referencing data from this script in others is very simple
//using the same method for other single Instances like the GameController or GameStateManager
if (Instance != null && Instance != this)
{
Destroy(this.gameObject); //only destroying the script is not enough as the Game Object still exists then
}
else
{
Instance = this;
}
}
// Start is called before the first frame update
void Start()
{
// this ensures that the instance of this script does not get destroyed on scene change (works in relation with the Singleton pattern)
DontDestroyOnLoad(this.gameObject);
lastBCISimulationChange = Time.time;
// initialize motion for simulation if HMD, leftHand, and rightHand are assigned
if (!(HMD == null || leftHand == null || rightHand == null))
{
lastHMDpos = HMD.transform.position;
lastHMDrot = HMD.transform.rotation;
lastLeftHandPos = leftHand.transform.position;
lastLeftHandRot = leftHand.transform.rotation;
lastRightHandPos = rightHand.transform.position;
lastRightHandRot = rightHand.transform.rotation;
}
else
{
Debug.LogWarning("HMD or hands are not assigned, cannot simulate focus based on motion!");
}
// initialize vectors
pastHMDposVel = new float[pastMovementVectorsAmount];
pastHMDrotVel = new float[pastMovementVectorsAmount];
pastleftHandposVel = new float[pastMovementVectorsAmount];
pastleftHandrotVel = new float[pastMovementVectorsAmount];
pastrightHandposVel = new float[pastMovementVectorsAmount];
pastrightHandrotVel = new float[pastMovementVectorsAmount];
//Sender logic initialization
if (!doSimulateBCI)
{
Debug.Log("Using real IXR focus stream!");
var hashStreamName = Hash128.Compute(senderStreamName);
var hashStreamType = Hash128.Compute(streamType);
int id = gameObject.GetInstanceID();
var hashObjectID = Hash128.Compute(id.ToString());
var hash = (hashStreamName, hashStreamType, hashObjectID).GetHashCode();
StreamInfo streamInfo = new StreamInfo(senderStreamName, streamType, 1, LSL.LSL.IRREGULAR_RATE,
channel_format_t.cf_string, hash.ToString());
outlet = new StreamOutlet(streamInfo);
Debug.Log("LSL event marker stream created");
}
}
// Update is called once per frame
void Update()
{
if (doSimulateBCI)
{
if (Time.time - lastBCISimulationChange >= BCISimulationIntervall)
{
SimulateBCI(BCISimulation);
}
if (useMotionForSimulation)
{
UpdateMovement();
focusLevel = GetSimulatedFocusValue();
}
else
{
focusLevel = currentBCIFocus;
}
}
else
{
PullDataFromInlet();
}
}
private void SimulateBCI(BCISimulationMethod method)
{
switch (method)
{
case BCISimulationMethod.random:
currentBCIFocus += UnityEngine.Random.Range(-0.1f, 0.1f);
currentBCIFocus = Mathf.Clamp(currentBCIFocus, 0.25f, 0.8f);
break;
case BCISimulationMethod.constant:
currentBCIFocus = 0.55f;
break;
default:
Debug.LogError("Incorrect BCI simulation method! Must be 'random' or 'constant'!");
break;
}
lastBCISimulationChange = Time.time;
}
private void PullDataFromInlet()
{
// if there is no inlet, search for one
if (inlet == null)
{
//Debug.Log("Resolving LSL stream");
// the streamName is of great importance because it is looking for channels with that exact name here!
resolver = new ContinuousResolver("name", streamName);
var results = resolver.results();
// create an inlet if there is one
if (results.Length > 0)
{
inlet = new StreamInlet(results[0]);
inlet.open_stream();
Debug.Log("IXR stream inlet created");
}
// if no inlet can be found, switch to simulated brainpower and delete the current resolver
else
{
// Debug.LogWarning("no inlet found! simulating brainpower...");
resolver = null;
if (Time.time - lastBCISimulationChange >= BCISimulationIntervall) SimulateBCI(BCISimulationMethod.random);
UpdateMovement();
focusLevel = GetSimulatedFocusValue();
}
}
// if there is an inlet, pull samples
if (inlet != null)
{
sample = new float[1];
double lastTimeStamp = inlet.pull_sample(sample, 0.0f);
// if the sample's value is not zero, set it as the focusLevel
if (sample[0] != 0)
{
if (disconnectTimer > 0) disconnectTimer = 0;
focusLevel = sample[0];
// Debug.Log("current IXR BCI focus: " + focusLevel);
}
// if the sample's value is zero, it may indicate a disconnection to the stream.
// for each value that equals zero, add time to a disconnect timer, to initiate a disconnect if they keep appearing (reset the timer if value != 0)
else
{
disconnectTimer += Time.deltaTime;
if (disconnectTimer >= disconnectTimeLimit)
{
Debug.LogWarning("DISCONNECT! Simulating BCI Focus now!");
disconnectTimer = 0;
// set the starting value for simulated brainpower
currentBCIFocus = 0.5f;
inlet.close_stream();
inlet = null;
}
}
}
}
private void UpdateMovement()
{
// Check if HMD, leftHand, and rightHand are assigned
if (HMD == null || leftHand == null || rightHand == null) return;
// Continue with the rest of your code if all objects are assigned...
currentHMDposVel = Vector3.Distance(lastHMDpos, HMD.transform.position) / Time.deltaTime;
for (int i = 0; i <= pastHMDposVel.Length - 2; i++)
{
pastHMDposVel[i] = pastHMDposVel[i + 1];
}
pastHMDposVel[pastHMDposVel.Length - 1] = currentHMDposVel;
currentHMDrotVel = Quaternion.Angle(lastHMDrot, HMD.transform.rotation) / Time.deltaTime;
for (int i = 0; i <= pastHMDrotVel.Length - 2; i++)
{
pastHMDrotVel[i] = pastHMDrotVel[i + 1];
}
pastHMDrotVel[pastHMDrotVel.Length - 1] = currentHMDrotVel;
// update left hand values
if (leftHand == null) return;
currentleftHandposVel = Vector3.Distance(lastLeftHandPos, leftHand.transform.position) / Time.deltaTime;
for (int i = 0; i <= pastleftHandposVel.Length - 2; i++)
{
pastleftHandposVel[i] = pastleftHandposVel[i + 1];
}
pastleftHandposVel[pastleftHandposVel.Length - 1] = currentleftHandposVel;
currentleftHandrotVel = Quaternion.Angle(lastLeftHandRot, leftHand.transform.rotation) / Time.deltaTime;
for (int i = 0; i <= pastleftHandrotVel.Length - 2; i++)
{
pastleftHandrotVel[i] = pastleftHandrotVel[i + 1];
}
pastleftHandrotVel[pastleftHandrotVel.Length - 1] = currentleftHandrotVel;
// update right hand values
if (rightHand == null) return;
currentrightHandposVel = Vector3.Distance(lastRightHandPos, rightHand.transform.position) / Time.deltaTime;
for (int i = 0; i <= pastrightHandposVel.Length - 2; i++)
{
pastrightHandposVel[i] = pastrightHandposVel[i + 1];
}
pastrightHandposVel[pastrightHandposVel.Length - 1] = currentrightHandposVel;
currentrightHandrotVel = Quaternion.Angle(lastRightHandRot, rightHand.transform.rotation) / Time.deltaTime;
for (int i = 0; i <= pastrightHandrotVel.Length - 2; i++)
{
pastrightHandrotVel[i] = pastrightHandrotVel[i + 1];
}
pastrightHandrotVel[pastrightHandrotVel.Length - 1] = currentrightHandrotVel;
// update last values to be current
lastHMDpos = HMD.transform.position;
lastHMDrot = HMD.transform.rotation;
lastLeftHandPos = leftHand.transform.position;
lastLeftHandRot = leftHand.transform.rotation;
lastRightHandPos = rightHand.transform.position;
lastRightHandRot = rightHand.transform.rotation;
}
public float GetSimulatedFocusValue()
{
// compute HMD values
foreach (float item in pastHMDposVel)
{
meanHMDPosVel += item;
}
meanHMDPosVel = meanHMDPosVel / pastHMDposVel.Length;
foreach (float item in pastHMDrotVel)
{
meanHMDRotVel += item;
}
meanHMDRotVel = meanHMDRotVel / pastHMDrotVel.Length;
// sigmoid formulas for vel:
currentHMDposFocus = -(movementFocusScale / 3) / (1 + (Mathf.Exp((-4 / posThreshold) * meanHMDPosVel + 4))) + ((movementFocusScale / 3) / 2);
currentHMDrotFocus = -(movementFocusScale / 3) / (1 + (Mathf.Exp((-4 / rotThreshold) * meanHMDRotVel + 4))) + ((movementFocusScale / 3) / 2);
// compute left hand values
foreach (float item in pastleftHandposVel)
{
meanleftHandPosVel += item;
}
meanleftHandPosVel = meanleftHandPosVel / pastleftHandposVel.Length;
foreach (float item in pastleftHandrotVel)
{
meanleftHandRotVel += item;
}
meanleftHandRotVel = meanleftHandRotVel / pastleftHandrotVel.Length;
// sigmoid formulas for vel:
currentleftHandposFocus = -(movementFocusScale / 3) / (1 + (Mathf.Exp((-4 / posThreshold) * meanleftHandPosVel + 4))) + ((movementFocusScale / 3) / 2);
currentleftHandrotFocus = -(movementFocusScale / 3) / (1 + (Mathf.Exp((-4 / rotThreshold) * meanleftHandRotVel + 4))) + ((movementFocusScale / 3) / 2);
// compute right hand values
foreach (float item in pastrightHandposVel)
{
meanrightHandPosVel += item;
}
meanrightHandPosVel = meanrightHandPosVel / pastrightHandposVel.Length;
foreach (float item in pastrightHandrotVel)
{
meanrightHandRotVel += item;
}
meanrightHandRotVel = meanrightHandRotVel / pastrightHandrotVel.Length;
// sigmoid formulas for vel:
currentrightHandposFocus = -(movementFocusScale / 3) / (1 + (Mathf.Exp((-4 / posThreshold) * meanrightHandPosVel + 4))) + ((movementFocusScale / 3) / 2);
currentrightHandrotFocus = -(movementFocusScale / 3) / (1 + (Mathf.Exp((-4 / rotThreshold) * meanrightHandRotVel + 4))) + ((movementFocusScale / 3) / 2);
// combine movement values
currentMovementFocus = currentHMDposFocus + currentHMDrotFocus +
currentleftHandposFocus + currentleftHandrotFocus +
currentrightHandposFocus + currentrightHandrotFocus;
// overall combine and clamp focus
focusLevel = currentBCIFocus + currentMovementFocus;
focusLevel = Mathf.Clamp(focusLevel, 0.05f, 1);
return focusLevel;
}
// Push event markers through the outlet
public void SendEventMarker(string eventName, string eventTask)
{
// the Python code for LSL expects that a message (messageToSend) is "name;task", with the name and task seperated by a semicolon!
// e.g. messageToSend = "event;senddata";
string messageToSend = eventName + ";" + eventTask;
Debug.Log("Sending Event marker: " + messageToSend);
if (outlet == null) return;
sampleString[0] = messageToSend;
outlet.push_sample(sampleString);
}
}