tl, dr: you either need a buffer that has really low Rds(on) to ensure that your series resistor is the only thing (or at least, the dominant thing) setting your current, or you need to sense the current and regulate it. That's easy enough, as you'll see below.
Also, to set the current in each direction you'll also need to steer the current so that each LED's forward drop is dealt with separately. That's not too hard either.
To address your second question about needing a buffer (yes, you need one), let's talk about the Rpi GPIO drive for a moment. Its datasheet specification only guarantees a max/min Voh/Vol at the rated current, not that it will deliver a specific current to a specific load.
What actually sets the Voh/Vol value is the internal resistance of the driver, Rds(on). Broadcom won't tell you that Rds(on) value, but you can figure it out by looking at Voh/Vol values at the rated current. Let's do that, using their '12mA' setting (3.3V I/O):
- '12mA' drive @Voh 2.62V, Rds(on) max = (3.3 - 2.62V) / 12mA =< 56.7 ohms
- '12mA' drive @Vol 0.5V, Rds(on) max = 0.5V / 12mA =< 41.7 ohms
Note that these are the maximum Rds(on) values. Rds(on) can be (and often is) much lower than that, to allow for process margin. Even with a lower Rds(on), its magnitude is comparable to the load resistance you're contemplating (60 ~ 100 ohms or so) for your LEDs. And it gets even worse if you want to use 'pulse overdrive' with higher current (more below.)
All that aside, because we want to steer the current and also to turn it on and off at each LED, we'll need at least 2 GPIOs for control, buffers or not.
So, what about those buffers then? FETs are available that have much stronger drive than the Rpi's GPIOs, with Rds(on) in the single digits range.
First idea: Cheap-and-cheerful, two FETs and two resistors. Easy to build, it'll be fairly accurate but somewhat inefficient (simulate it here):
Design Notes:
- Use 1% resistors
- Use BSS138 FETs or similar. BSS138 has Rds(on) about < 2.5 ohms or less at Vgs > 3V
- You could also use 2N3904 or 2N2222 type NPN bipolar, if you compensate for Vce(sat) with the resistors.
This circuit gives reasonable accuracy by using 1% dropping resistors and FETs with Rds(on) low enough that they aren't a significant contributor to the load resistance. Nevertheless it is still sensitive to LED Vf variation and power supply fluctuation.
Second idea: If you want better repeatability you have to compensate for Vf variance and the power supply. Here's a design that can do that (simulate it here):
This would be a more production-worthy circuit. It senses the LED current and sets it precisely vs. a local reference. The switch is a dual analog type, the LVC2G66. The current sense feedback compensates for the switch 'on' resistance, up to 20 ohms for this switch.
Design Notes:
- The op-amp type is a low-voltage, rail-to-rail version of the LM324 (LMV32xx). It has enough output current to do the job on its own.
- More-common op-amps could be used if VDD were 5V or more.
- The voltage divider at left can be adjusted to change the current.
A possible upgrade would be a precision reference instead of the voltage divider; it probably isn't needed if the static 3.3V can be calibrated out at start-up.
Third idea: A possible upgrade that your professor hinted at is to use a pulsed LED current. We can go a step further and use a higher 'pulsed overdrive' to get even better illumination and efficiency. LEDs can accommodate overdrive so long as the average power is within the datasheet spec. (example: 40mA at 50% duty, 60mA at 33% duty, etc.)
We start with the current-sensing design and beef it up with lower Rds(on) switches and an external pass transistor for the op-amp. Here's an example that's set up for about 60mA (simulate it here):
You'd run this at no more than a 33% duty cycle (I suggest 25% to reduce power) to keep the LED power within the 20mA steady-state spec. Caution: mind the default GPIO state to make sure you don't burn the LEDs. Bonus: the external pass transistor allows using the plain old LM324 op-amp, so it might be easier to build.
Fourth idea: Finally, this Maxim Integrated appnote may be of interest to you: https://www.maximintegrated.com/en/design/technical-documents/tutorials/4/4671.html