Determining Photogate Timing Uncertainty

Determining Photogate Timing Uncertainty

In order to do an uncertainty analysis of Front Range’s conservation of momentum experiments, we need to determine the uncertainty of the times reported by the photogates. The key to experiments of this sort is to collect redundant measurements, use least squares techniques to come up with a model of the expected results and then take statistics on the differences between the measurements and the fitted model.

The results, developed below, show that the photogate timing uncertainty is roughly 1.3 ms, and that the resistance force is proportional to the glider’s velocity.

The experiment to determine the photogate timing uncertainties uses four photogates spaced evenly along an air track, connected to a laptop computer via a USB data acquisition system. A glider slides along the air track, and a fin on the glider interrupts a beam of infrared light across the photogate. The data acquisition system records the times that the beam transitions between the broken and clear states. The air track provides the glider with a low-friction environment, but the resistance is not zero, so we will model the glider as having a constant decelleration during each pass along the track.

In the experiment, the glider is given an initial velocity, and allowed to bounce off the ends of the track, reversing direction each time. The data acquistion system collects data for 60 seconds, allowing on the order of 10 round trips along the track for each run.

load run5
n = length(t_meas)
n_trips = fix(n/16)
n = n_trips*16
t_meas = t_meas(1:n);
l_fin = 0.07; % 7 cm fin length
t_est = (t_meas(1:2:end)+t_meas(2:2:end))/2;
v_est = l_fin./(t_meas(2:2:end) - t_meas(1:2:end));
plot(t_est, v_est, '+')
xlabel('Time (s)')
ylabel('Velocity (m/s)')
title('Run 5 velocity history from time measurements')

n =


n_trips =


n =



Estimating Photogate Spacing

The glider goes through photogates 1, 2, 3 and 4, and then bounces off the far end of the track and goes through photogates 4, 3, 2 and 1. It then bounces off the near end of the track and repeats the process. I should be able to estimate the spacing of the photogates from the measurements, but I need to be sure to use the correct times to work out the three distances.

% Spacing between photogates 1 and 2, travelling right
d1_right = (v_est(2:8:end)+v_est(1:8:end))/2.*(t_est(2:8:end)-t_est(1:8:end));
% Spacing between photogates 1 and 2, travelling left
d1_left = (v_est(7:8:end)+v_est(8:8:end))/2.*(t_est(8:8:end)-t_est(7:8:end));
d1_data = [d1_right; d1_left];
d1 = mean(d1_data)
d1_unc = std(d1_data)

% Spacing between photogates 2 and 3, traveling right
d2_right = (v_est(3:8:end)+v_est(2:8:end))/2.*(t_est(3:8:end)-t_est(2:8:end));
% Spacing between photogates 2 and 3, traveling left
d2_left = (v_est(6:8:end)+v_est(7:8:end))/2.*(t_est(7:8:end)-t_est(6:8:end));
d2_data = [d2_right; d2_left];
d2 = mean(d2_data)
d2_unc = std(d2_data)

% Spacing between photogates 3 and 4, traveling right
d3_right = (v_est(4:8:end)+v_est(3:8:end))/2.*(t_est(4:8:end)-t_est(3:8:end));
% Spacing between photogates 3 and 4, traveling left
d3_left = (v_est(5:8:end)+v_est(6:8:end))/2.*(t_est(6:8:end)-t_est(5:8:end));
d3_data = [d3_right; d3_left];
d3 = mean(d3_data)
d3_unc = std(d3_data)

d1 =


d1_unc =


d2 =


d2_unc =


d3 =


d3_unc =


Analyzing the First Rightward Pass

This will take a few steps…

Define the Locations of Block and Clear Events

t_pass = t_meas(1:8) - mean(t_meas(1:8))
x_photogate = d2*[-1 -1 +1 +1]/2 + [-d1 0 0 d3]
A = [1 0 0 0; 1 0 0 0; 0 1 0 0; 0 1 0 0; 0 0 1 0; 0 0 1 0; 0 0 0 1; 0 0 0 1]';
x_event = x_photogate*A + [-1 +1 -1 +1 -1 +1 -1 +1]*l_fin/2

t_pass =


x_photogate =

   -0.3588   -0.1190    0.1190    0.3612

x_event =

   -0.3938   -0.3238   -0.1540   -0.0840    0.0840    0.1540    0.3262    0.3962

Basic Linear Fit

% Find the midpoints of the occluded intervals
t_mid = (t_pass(1:2:end) + t_pass(2:2:end))/2
% Now find estimated velocities
v_est = l_fin./(t_pass(2:2:end) - t_pass(1:2:end))
% Naive fit
[v_fit, v_unc, a, v_0, a_unc, v_0_unc] = uncertainFit(t_mid, v_est);
plot(t_mid, v_est, '+-', t_mid, v_fit, 'x:')
xlabel('Time (s)')
ylabel('Velocity (m/s)')
title('Naive Velocity Fit')

t_mid =


v_est =



Find an initial condition to minimize the squared error

I’ll use the fitted acceleration and velocity to predict the event times based on a range of initial positions, and then choose the initial position that minimizes the sum of the squares of the differences between the estimated and measured event times.

t_pred = t_pass; % create array of correct size, the values will be replaced
x_init = (-5:5)*d2/240;
sse_val = x_init*0; % again, creating a correctly sized array
for j = 1:length(x_init)
  for i = 1:8
    t_pred(i) = 2*(x_event(i)-x_init(j))/(v_0 + sqrt(v_0^2 + 2*a*(x_event(i)-x_init(j))));
  sse_val(j) = sum((t_pred-t_pass).^2);
plot(x_init, sse_val, 'x-')
title('First attempt at finding best initial condition')
xlabel('Initial position (m)')
ylabel('RSS error')

p = polyfit(x_init, sse_val, 2)
x_best = -p(2)/p(1)/2
best_sse_est = polyval(p, x_best)

p =

   15.1811   -0.0542    0.0000

x_best =


ans =


best_sse_est =



Now find an improved estimate of the initial position

for i = 1:8
  t_pred(i) = 2*(x_event(i)-x_best)/(v_0 + sqrt(v_0^2 + 2*a*(x_event(i)-x_best)));

best_sse_calc = sum((t_pred-t_pass).^2)
plot(x_init, sse_val-polyval(p, x_init), 'x-', x_best, best_sse_est, 'o')

x_init = (-5:5)*d2/960+x_best;
sse_val = x_init*0;
for j = 1:length(x_init)
  for i = 1:8
    t_pred(i) = 2*(x_event(i)-x_init(j))/(v_0 + sqrt(v_0^2 + 2*a*(x_event(i)-x_init(j))));
  sse_val(j) = sum((t_pred-t_pass).^2);
plot(x_init, sse_val, 'x-')
title('Refined attempt at finding best initial condition')
xlabel('Initial position (m)')
ylabel('RSS error')

best_sse_calc =



Use a minimization routine to find best x_0, v_0 and a

The calculations above suggest that finding values of initial position, initial velocity and acceleration that minimize the sum of the squares between modeled and measured times. Finding an analytical expression for the minimizing values would be challenging, but a minimization algorithm can find these values quickly.

v = version; % for checking if it's running in Matlab or Octave

Loop through all passes along track

x_best_list = [];
v_best_list = [];
a_best_list = [];
e_best_list = [];
sse_list = [];
x_fwd = x_event;
x_back = -x_event(8:-1:1);
for i = 1:n_trips
    i_offset = (i-1)*16;
    % forward trip
    t_pass = t_meas(i_offset+1:i_offset+8);
    t_pass = t_pass-mean(t_pass);
    t_mid = (t_pass(1:2:end) + t_pass(2:2:end))/2;
    v_est = l_fin./(t_pass(2:2:end)-t_pass(1:2:end));
    [v_fit, v_unc, a, v_0, a_unc, v_0_unc] = uncertainFit(t_mid, v_est);
    if v(1) == '7' % it's Matlab
        [X_best, sse_best] = fminsearch(@(x) sse(x, x_fwd, t_pass), [0 v_0 a]);
        % Nelder-Mead unconstrained nonlinear minimization
    elseif v(1) == '3' % it's Octave
        [X_best, sse_best, info, iter, nf, lambda] = sqp ([0 v_0 a]', @(x) sse(x, x_fwd, t_pass));
        % sequential quadratic programming algorithm
        warning('Unknown environment')
    x_best_list = [x_best_list X_best(1)];
    v_best_list = [v_best_list X_best(2)];
    a_best_list = [a_best_list X_best(3)];
    e_best_list = [e_best_list sse_best];
    % backward trip
    t_pass = t_meas(i_offset+9:i_offset+16);
    t_pass = t_pass-mean(t_pass);
    t_mid = (t_pass(1:2:end) + t_pass(2:2:end))/2;
    v_est = l_fin./(t_pass(2:2:end)-t_pass(1:2:end));
    [v_fit, v_unc, a, v_0, a_unc, v_0_unc] = uncertainFit(t_mid, v_est);
    if v(1) == '7' % it's Matlab
        [X_best, sse_best] = fminsearch(@(x) sse(x, x_back, t_pass), [0 v_0 a]);
        % Matlab uses Nelder-Mead unconstrained nonlinear minimization
    elseif v(1) == '3' % it's Octave
        [X_best, sse_best, info, iter, nf, lambda] = sqp ([0 v_0 a]', @(x) sse(x, x_back, t_pass));
        % Octave uses a sequential quadratic programming algorithm
        warning('Unknown environment')
    x_best_list = [x_best_list X_best(1)];
    v_best_list = [v_best_list X_best(2)];
    a_best_list = [a_best_list X_best(3)];
    e_best_list = [e_best_list sse_best];
end % of the n_trips/2 round trips along the track

The Results

The main goal was to determine the timing uncertainty of the photogate measurements, but the multiple passes at decreasing speeds allow us to make an estimate of the resistance force as a function of glider velocity. Before doing those calculations, let’s look at the results.

The optimization algorithm provided the resulting sum of the squares of the timing discrepancies, so these provide the information needed to find the timing uncertainty. The number of data points (8 per one-way pass) minus the number of fitted parameters (3: position, velocity and acceleration) gives the number of degrees of freedom to divide the sum by.

t_unc = sqrt(sum(e_best_list)/(5*n_trips*2)) % the uncertainty estimate
m = 0.1478; % kg (mass of glider)
m_unc = 0.00005; % kg (uncertainty of mass measurements)
F = -m*a_best_list;
plot(v_best_list, F, '+-')
title('Analyzing the Resistance Force')
xlabel('Velocity (m/s)')
ylabel('Force (N)')

t_unc =



This is a disturbing result. It looks like the forward and backwards trips have different acceleration vs velocity profiles. The likely cause of this is a tilted track; while we made an effort to level the track, it apparently wasn’t as level as we had hoped. To estimate the acceleration bias, take the average of the acceleration differences between the forward and backward passes:

F_bias = mean(F(1:2:end)-F(2:2:end))/2
F_adj = F - F_bias*[1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1 1 -1];

F_bias =


The results are close enough that we can make a plausible force vs. velocity fit. Theoretically, aerodynamic drag results in a quadratic dependance on velocity, while viscosity in the layer of air between the glider and track would give a linear relationship between force and velocity. Let’s check both.

p_quad = polyfit(v_best_list, F_adj, 2);
v_quad = (0:0.05:1)*max(v_best_list);
F_quad = polyval(p_quad, v_quad);
[F_lin, F_unc, C_v, F_0, C_v_unc, F_0_unc] = uncertainFit(v_best_list, F_adj)

plot(v_best_list, F_adj, '+-', v_quad, F_quad, '--', v_quad, C_v*v_quad+F_0, ':')
legend('Data', 'Quadratic fit', 'Linear fit', 'Location', 'Northwest')
title('Force after removing bias')
xlabel('Velocity (m/s)')
ylabel('Force (N)')

F_lin =

  Columns 1 through 8 

    0.0018    0.0018    0.0017    0.0016    0.0015    0.0015    0.0014    0.0013

  Columns 9 through 16 

    0.0013    0.0012    0.0011    0.0011    0.0010    0.0010    0.0009    0.0009

  Columns 17 through 22 

    0.0008    0.0008    0.0007    0.0007    0.0006    0.0006

F_unc =


C_v =


F_0 =


C_v_unc =


F_0_unc =


ans =



The quadratic fit between velocity and force shows a substantial force at zero velocity, which seems unlikely. The linear fit nearly goes through zero, and in fact the uncertainty in the zero velocity force is greater than the magnitude of the zero velocity force, suggesting that the resistance force is a simple multiple of the velocity.

Published with MATLABĀ® 7.1

Posted in Uncategorized | Tagged , , | Leave a comment


I’ve figured out how to make movies from my OpenGL animation, and I’ve posted a video on YouTube, here.

I’ll be giving a talk on the work on Monday, January 24th at CU Boulder, at the Astrodynamics and Satellite Navigation Seminar. It’s at 3:00 pm, probably in the Seebass Forum Room (ECAE 153).

Posted in OpenGL, Uncategorized | Leave a comment

Visualizing Rotation of a Rigid Body

My fans (yes, both of you! Or am I down to one? zero?) may be wondering where I’ve gotten to. I’ve been working up a visualization program for a conference in Nanjing, China.

Back in grad school, I was an aerospace engineer, working in spacecraft dynamics and control. Torque-free rotation of a rigid body is one of those basic physics topics that we studied, on the way to figuring out how to control rotational motion of satellites and such.

My PhD advisor has gotten a big award, to be presented at a conference in Nanjing, and they’d like to have his students and colleagues come to present. Not having worked in the aerospace business for a while, I don’t have any research results to present, but I thought it would be fun to use OpenGL to visualize some of the analytical solutions to the rigid body rotation problem.

Recall what a spinning football does. If you get it spinning exactly around the long axis (the “perfect spiral”), it just spins around that axis. More often, it gets that funny wobble. How do we figure that wobble out?

One way to visualize the motion is to think about the angular momentum and angular velocity vectors (not the same, unfortunately, unless you have a spherically-symmetric object, which footballs aren’t). The angular momentum vector is constant in inertial space (what you’d see outside the vehicle, if you could see angular momentum vectors), but isn’t in a body-fixed frame (what you’d see if you were strapped into the vehicle).

What’s the angular velocity vector? If you think of the object as spinning on an imaginary axle, the angular velocity vector points along the axle, and its length is proportional to the speed of rotation. This axle usually has to be imaginary, since its direction can change relative to the body.

So I’ve worked up a simulation of a rotating, rigid body, with angular momentum and angular velocity vectors added.

A rotating body, with its angular momentum shown as a yellow arrow, and its angular velocity shown as a purple arrow.

This is a frame of the animated image. The yellow arrow is the angular momentum vector, and the purple arrow is the angular velocity vector. The red and yellow ice cream cone thing is the “spotlight” that illuminates the scene.

More images and explanations to come as I make progress with the visualizations!

Posted in OpenGL | Leave a comment

Experimenting with Materials

I’ve been having trouble getting the specular highlights to show up on translucent ellipsoids, so I decided to create a program to let me vary the material colors and light strengths with sliders. I partly based it on Spot, but started from scratch because I wanted to control the material properties and lighting intensities in detail. At first I couldn’t get anything to show up in the OpenGL view, it showed up only as a pure white rectangle. I only found the problem after an almost line-by-line comparison of the new code with Spot. At very end of drawRect:, I had forgotten glSwapAPPLE(), so all of that drawing I did never made it to the window.

At first it seemed that specular lighting wasn’t working, but eventually I realized the problem was that the ambient and diffuse components were overwhelming the specular component: apparently the color has maximum level, and once it reaches that, no additional components will make that part of the object brighter.

Demonstrating Materials and Lighting

It was a great help to have the sliders controlling the lighting and material settings in real time, instead of having to go through the edit-compile-link-run cycle. I also had the settings saved in the ID field of the TGA files, so I can (theoretically, at least) refer to them later on. Actually doing it, though, will require me to create a TGA image reader app that will display the ID text along with the image. Yet another app to create, more practice!

Posted in Uncategorized | Leave a comment

New Book: iPhone 3D Programming

I’ve been working through the OpenGL SuperBible, Fourth Edition, and in order to get to shaders and begin working in OpenGL ES on the iOS, I decided to pick up a copy of iPhone 3D Programming, by Philip Rideout, a Denver author, as it turns out. I’m moving on to this book because my goal is iOS development, and this book gets me there more directly than studying OpenGL and iOS separately.

This book jumps right into application development, with HelloArrow, which puts a sort-of Starship Enterprise symbol on screen, keeping it vertical as the device rotates. There are actually two implementations, one in OpenGL ES 1.1, for earlier iOS devices, and the second in OpenGL ES 2.0, the modern version that uses shaders, which are small C-like programs that are compiled to run directly on the graphics hardware instead of the CPU. Shaders replace the fixed-function pipeline originally used in OpenGL. They were introduced in OpenGL 1.4 as an extension, and became part of the OpenGL core in version 2.0.

The book makes extensive use of C++ to do the heavy lifting for rendering and associated computational tasks, with just enough Objective-C to interface the rendering with iOS. You’ll want to be up on C++ to make your way through the book. Knowing something about Design Patterns will be very helpful—having studied Head First Design Patterns let me recognize what was going on in chapter 1.

The payoff for using C++ is portability: it’s easier to use C++ on other mobile devices, since Objective-C is very much an OS X and iOS peculiarity. I’ve gotten fond of Objective-C since I’ve been using it regularly, but this book is reminding me how powerful C++ is, and how much I enjoyed learning and using it.

The first version of the app uses a C++ renderer based on OpenGL ES 1.1. The second version upgrades to OpenGL ES 2.0, with code that checks if 2.0 is available, and dropping back to 1.1 if necessary.

The errors I came across were all fairly trivial, although hard to find. I had mis-capitalized some variable names, turning the correct Modelview into ModelView, and that kept the symbol from displaying: all I got was the gray background.

Here’s the final result:

HelloArrow: a yellow arrow on a gray background

Posted in iOS, OpenGL | Leave a comment

Chapter 8

Chapter 8 covers texturing, the process of applying an image to a surface, instead of simply making it a solid color. This can improve an image by adding a lot of detail without adding a huge number of triangles with their attendant supply of vertexes, normals and color data.

The first sample program applies a stone pattern to a pyramid. I adjusted the proportions of the pyramid to match the Great Pyramid at Giza, and then modified it to look like the dollar bill pyramid, with the apex floating above the base, like this:

The basic pyramid has five points and six triangles (four faces and two on the square base), but the floating-apex pyramid requires a lot more effort. I wrote a triangle-drawing method that takes three vertex index values, which then set the normal to the triangle, calculated the texture coordinates for each vertex, and then drew the three vertexes. That made the drawPyramid method much clearer: it just had to draw triangles, and the heavy lifting got done behind the scenes. Here’s the code:

- (void)drawTrianglePointA:(int)iA 
                    pointC:(int)iC {
    M3DVector3f vNormal;
    m3dFindNormal(vNormal, vCorners[iA], vCorners[iB], vCorners[iC]);
    // NSLog(@"Triangle %i, %i, %i with normal (%f, %f, %f)", 
    //      iA, iB, iC, vNormal[0], vNormal[1], vNormal[2]);
    int sAxis, tAxis;
    float sFactor, tFactor;
    float sBias, tBias;
    float s, t;
    if (vNormal[1] > 0.8 | vNormal[1] < -0.8) { // top of apex
        sAxis = 0;
        tAxis = 2;
        sFactor = tFactor = vNormal[1]/(2.0f*width);
        sBias = tBias = 0.5f;
    } else if (vNormal[0] > 0.7f) {
        sAxis = 2;
        tAxis = 1;
        sFactor = -0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    } else if (vNormal[0] < -0.7f) {
        sAxis = 2;
        tAxis = 1;
        sFactor = +0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    } else if (vNormal[2] > 0.7f) {
        sAxis = 0;
        tAxis = 1;
        sFactor = +0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    } else {
        sAxis = 0;
        tAxis = 1;
        sFactor = -0.5f/width;
        sBias = 0.5f;
        tFactor = 1.0f/height;
        tBias = 0.0f;
    // NSLog(@"sAxis = %i, tAxis = %i, sFactor, sBias = %f, %f; tFactor, tBias = %f, %f",
    //      sAxis, tAxis, sFactor, sBias, tFactor, tBias);
    s = vCorners[iA][sAxis]*sFactor + sBias;
    t = vCorners[iA][tAxis]*tFactor + tBias;
    // NSLog(@"s = %f, t = %f", s, t);
    glTexCoord2f(s, t);

    s = vCorners[iB][sAxis]*sFactor + sBias;
    t = vCorners[iB][tAxis]*tFactor + tBias;
    // NSLog(@"s = %f, t = %f", s, t);
    glTexCoord2f(s, t);

    s = vCorners[iC][sAxis]*sFactor + sBias;
    t = vCorners[iC][tAxis]*tFactor + tBias;
    // NSLog(@"s = %f, t = %f", s, t);
    glTexCoord2f(s, t);

- (void)drawPyramid {
    // bottom of apex
    [self drawTrianglePointA:2 pointB:4 pointC:1];
    [self drawTrianglePointA:2 pointB:3 pointC:4];

    // faces of apex
    [self drawTrianglePointA:0 pointB:4 pointC:3];
    [self drawTrianglePointA:0 pointB:1 pointC:4];
    [self drawTrianglePointA:0 pointB:2 pointC:1];
    [self drawTrianglePointA:0 pointB:3 pointC:2];

    // Top of base
    [self drawTrianglePointA:6 pointB:5 pointC:8];
    [self drawTrianglePointA:8 pointB:7 pointC:6];

    // faces of base
    [self drawTrianglePointA:8 pointB:12 pointC:7];
    [self drawTrianglePointA:7 pointB:12 pointC:11];
    [self drawTrianglePointA:5 pointB:9  pointC:8];
    [self drawTrianglePointA:8 pointB:9  pointC:12];
    [self drawTrianglePointA:6 pointB:10 pointC:5];
    [self drawTrianglePointA:5 pointB:10 pointC:9];
    [self drawTrianglePointA:7 pointB:11 pointC:6];
    [self drawTrianglePointA:6 pointB:11 pointC:10];

    // bottom of base
    [self drawTrianglePointA:10 pointB:12 pointC:9 ];
    [self drawTrianglePointA:10 pointB:11 pointC:12];

Here are the coordinates:

vCorners = {{    0.000000,   280.000000,     0.000000},
            {  -52.380951,   213.333344,   -52.380951},
            {   52.380951,   213.333344,   -52.380951},
            {   52.380951,   213.333344,    52.380951},
            {  -52.380951,   213.333344,    52.380951},
            {  -83.809525,   173.333344,   -83.809525},
            {   83.809525,   173.333344,   -83.809525},
            {   83.809525,   173.333344,    83.809525},
            {  -83.809525,   173.333344,    83.809525},
            { -220.000000,     0.000000,  -220.000000},
            {  220.000000,     0.000000,  -220.000000},
            {  220.000000,     0.000000,   220.000000},
            { -220.000000,     0.000000,   220.000000}};

The apex really needs to be casting a shadow to make the image more believable.

Now I need to add an eye texture to one face of the apex, and maybe “Novus Ordo Seclorum” or “MDCCLXXVI” at the bottom of the base. Or I could add it to SphereWorld and make the apex rotate…

Posted in OpenGL | Leave a comment

Querying OpenGL: Version and Extensions

OpenGL exists in a variety of versions, and different vendors have created extensions that other vendors don’t support. So how do you tell what verions of OpenGL you have, and what extensions can you use?

OpenGL lets you query its capabilities via the function glGetString(), which returns a string you can examine. Set the argument to the function to determine what OpenGL will tell you. GL_VERSION and GL_EXTENSIONS are the ones we’re interested in right now.

The single line of code NSLog(@"OpenGL Version %s", glGetString(GL_VERSION)); will tell you the version of OpenGL you have. My machine returns OpenGL Version 2.1 NVIDIA-1.6.18, so I’ve got an NVIDIA graphics card.

Here’s the code to develop the list of extensions.

   NSLog(@"Now checking extensions...");
   NSString *extensionString = [NSString 
     stringWithCString:(const char*)glGetString(GL_EXTENSIONS) 
   NSArray *extensions = [extensionString componentsSeparatedByString:@" "];
   NSString *extensionsInHTML;
   extensionsInHTML = [extensions componentsJoinedByString:@"</li>\n<li>"];
   NSLog(@"<ol>\n<li>%@</li>\n</ol>", extensionsInHTML);

Yes, I was in full nerd mode writing code to create HTML markup. My machine has 122 extensions, so I’ll spare you my list: you’re interested in your machine, of course.

Posted in OpenGL | Leave a comment