Bezier Patches / Fullscreen FixBezier Patches Written by: David Nikdel ( ogapo@ithink.net ) This tutorial is intended to introduce you to Bezier Surfaces in the hopes that someone more artistic than myself will do something really cool with them and show all of us. This is not intended as a complete Bezier patch library, but more as proof of concept code to get you familiar with how these curved surfaces actually work. Also, as this is a very informal piece, I may have occasional lapses in correct terminology in favor of comprehensability; I hope this sits well with everyone. Finally, to those of you already familiar with Beziers who are just reading this to see if I screw up, shame on you ;-), but if you find anything wrong by all means let me or NeHe know, after all no one's perfect, eh? Oh, and one more thing, none of this code is optimized beyond my normal programming technique, this is by design. I want everyone to be able to see exactly what is going on. Well, I guess that's enough of an intro. On with the show! The Math - ::evil music:: (warning, kinda long section) Ok, it will be very hard to understand Beziers without at least a basic understanding of the math behind it, however, if you just don't feel like reading this section or already know the math, you can skip it. First I will start out by describing the Bezier curve itself then move on to how to create a Bezier Patch. Odds are, if you've ever used a graphics program you are already familiar with Bezier curves, perhaps not by that name though. They are the primary method of drawing curved lines and are commonly represented as a series of points each with 2 points representing the tangent at that point from the left and right. Here's what one looks like: This is the most basic Bezier curve possible (longer ones are made by attaching many of these together (many times without the user realizing it)). This curve is actually defined by only 4 points, those would be the 2 ending control points and the 2 middle control points. To the computer, all the points are the same, but to aid in design we often connect the first and the last two, respectively, because those lines will always be tangent to the endpoint. The curve is a parametric curve and is drawn by finding any number of points evenly spaced along the curve and connecting them with straight lines. In this way you can control the resolution of the patch (and the amount of computation). The most common way to use this is to tesselate it less at a farther distance and more at a closer distance so that, to the viewer, it always appears to be a perfectly curved surface with the lowest possible speed hit. Bezier curves are based on a basis function from which more complicated versions are derived. Here's the function: t + (1 - t) = 1 Sounds simple enough huh? Well it really is, this is the Bezier most basic Bezier curve, a 1st degree curve. As you may have guessed from the terminology, the Bezier curves are polynomials, and as we remember from algebra, a 1st degree polynomial is just a straight line; not very interesting. Well, since the basis function is true for all numbers t, we can square, cube, whatever, each side and it will still be true right? Well, lets try cubing it. (t + (1-t))^3 = 1^3 t^3 + 3*t^2*(1-t) + 3*t*(1-t)^2 + (1-t)^3 = 1 This is the equation we use to calculate the most common Bezier, the 3rd degree Bezier curve. This is most common for two reasons, a) it's the lowest degree polynomial that need not necesarily lie in a plane (there are 4 control points) and b) the tangent lines on the sides are not dependant on one another (with a 2nd degree there would be only 3 control points). So do you see the Bezier curve yet? Hehe, me neither, that's because I still need to add one thing. Ok, since the entire left side is equal to 1, it's safe to assume that if you add all the components they should still equal one. Does this sound like it could be used to decide how much of each control point to use in calculating a point on the curve? (hint: just say yes ;-) ) Well you're right! When we want to calculate the value of a point some percent along the curve we simply multiply each part by a control point (as a vector) and find the sum. Generally, we'll work with 0 P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = Pnew Because polynomials are always continuous, this makes for a good way to morp between the 4 points. The only points it actually reaches though are P1 and P4, when t = 1 and 0 respectively. Now, that's all well and good, but how can I use these in 3D you ask? Well it's actually quite simple, in order to form a Bezier patch, you need 16 control points (4*4), and 2 variables t and v. What you do from there is calculate a point at v along 4 of the parallel curves then use those 4 points to make a new curve and calculate t along that curve. By calculating enough of these points, we can draw triangle strips to connect them, thus drawing the Bezier patch.
Well, I suppose that's enough math for now, on to the code! #include <windows.h> // Header File For Windows #include <math.h> // Header File For Math Library Routines #include <stdio.h> // Header File For Standard I/O Routines #include <stdlib.h> // Header File For Standard Library #include <gl\gl.h> // Header File For The OpenGL32 Library #include <gl\glu.h> // Header File For The GLu32 Library #include <gl\glaux.h> // Header File For The Glaux Library typedef struct point_3d { // Structure For A 3-Dimensional Point ( NEW ) double x, y, z; } POINT_3D; typedef struct bpatch { // Structure For A 3rd Degree Bezier Patch ( NEW ) POINT_3D anchors[4][4]; // 4x4 Grid Of Anchor Points GLuint dlBPatch; // Display List For Bezier Patch GLuint texture; // Texture For The Patch } BEZIER_PATCH; HDC hDC=NULL; // Private GDI Device Context HGLRC hRC=NULL; // Permanent Rendering Context HWND hWnd=NULL; // Holds Our Window Handle HINSTANCE hInstance; // Holds The Instance Of The Application DEVMODE DMsaved; // Saves The Previous Screen Settings ( NEW ) bool keys[256]; // Array Used For The Keyboard Routine bool active=TRUE; // Window Active Flag Set To TRUE By Default bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default GLfloat rotz = 0.0f; // Rotation About The Z Axis BEZIER_PATCH mybezier; // The Bezier Patch We're Going To Use ( NEW ) BOOL showCPoints=TRUE; // Toggles Displaying The Control Point Grid ( NEW ) int divs = 7; // Number Of Intrapolations (Controls Poly Resolution) ( NEW ) LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc The following are just a few quick functions for some simple vector math. If you're a fan of C++ you might consider using a point class (just make sure it's 3d). // Adds 2 Points. Don't Just Use '+' ;) POINT_3D pointAdd(POINT_3D p, POINT_3D q) { p.x += q.x; p.y += q.y; p.z += q.z; return p; } // Multiplies A Point And A Constant. Don't Just Use '*' POINT_3D pointTimes(double c, POINT_3D p) { p.x *= c; p.y *= c; p.z *= c; return p; } // Function For Quick Point Creation POINT_3D makePoint(double a, double b, double c) { POINT_3D p; p.x = a; p.y = b; p.z = c; return p; } This is basically just the 3rd degree basis function written in C, it takes a variable u and an array of 4 points and computes a point on the curve. By stepping u in equal increments between 0 and 1, we'll get a nice approximation of the curve. // Calculates 3rd Degree Polynomial Based On Array Of 4 Points // And A Single Variable (u) Which Is Generally Between 0 And 1 POINT_3D Bernstein(float u, POINT_3D *p) { POINT_3D a, b, c, d, r; a = pointTimes(pow(u,3), p[0]); b = pointTimes(3*pow(u,2)*(1-u), p[1]); c = pointTimes(3*u*pow((1-u),2), p[2]); d = pointTimes(pow((1-u),3), p[3]); r = pointAdd(pointAdd(a, b), pointAdd(c, d)); return r; } This function does the lion's share of the work by generating all the triangle strips and storing them in a display list. We do this so that we don't have to recalculate the patch each frame, only when it changes. By the way, a cool effect you might want to try might be to use the morphing tutorial to morph the patch's control points. This would yield a very cool smooth, organic, morphing effect for relatively little overhead (you only morph 16 points, but you have to recalculate). The "last" array is used to keep the previous line of points (since a triangle strip needs both rows). Also, texture coordinates are calculated by using the u and v values as the percentages (planar mapping). One thing we don't do is calculate the normals for lighting. When it comes to this, you basically have two options. The first is to find the center of each triangle, then use a bit of calculus and calculate the tangent on both the x and y axes, then do the cross product to get a vector perpendicular to both, THEN normalize the vector and use that as the normal. OR (yes, there is a faster way) you can cheat and just use the normal of the triangle (calculated your favorite way) to get a pretty good approximation. I prefer the latter; the speed hit, in my opinion, isn't worth the extra little bit of realism. // Generates A Display List Based On The Data In The Patch // And The Number Of Divisions GLuint genBezier(BEZIER_PATCH patch, int divs) { int u = 0, v; float py, px, pyold; GLuint drawlist = glGenLists(1); // Make The Display List POINT_3D temp[4]; POINT_3D *last = (POINT_3D*)malloc(sizeof(POINT_3D)*(divs+1)); // Array Of Points To Mark The First Line Of Polys if (patch.dlBPatch != NULL) // Get Rid Of Any Old Display Lists glDeleteLists(patch.dlBPatch, 1); temp[0] = patch.anchors[0][3]; // The First Derived Curve (Along X-Axis) temp[1] = patch.anchors[1][3]; temp[2] = patch.anchors[2][3]; temp[3] = patch.anchors[3][3]; for (v=0;v<=divs;v++) { // Create The First Line Of Points px = ((float)v)/((float)divs); // Percent Along Y-Axis // Use The 4 Points From The Derived Curve To Calculate The Points Along That Curve last[v] = Bernstein(px, temp); } glNewList(drawlist, GL_COMPILE); // Start A New Display List glBindTexture(GL_TEXTURE_2D, patch.texture); // Bind The Texture for (u=1;u<=divs;u++) { py = ((float)u)/((float)divs); // Percent Along Y-Axis pyold = ((float)u-1.0f)/((float)divs); // Percent Along Old Y Axis temp[0] = Bernstein(py, patch.anchors[0]); // Calculate New Bezier Points temp[1] = Bernstein(py, patch.anchors[1]); temp[2] = Bernstein(py, patch.anchors[2]); temp[3] = Bernstein(py, patch.anchors[3]); glBegin(GL_TRIANGLE_STRIP); // Begin A New Triangle Strip for (v=0;v<=divs;v++) { px = ((float)v)/((float)divs); // Percent Along The X-Axis glTexCoord2f(pyold, px); // Apply The Old Texture Coords glVertex3d(last[v].x, last[v].y, last[v].z); // Old Point last[v] = Bernstein(px, temp); // Generate New Point glTexCoord2f(py, px); // Apply The New Texture Coords glVertex3d(last[v].x, last[v].y, last[v].z); // New Point } glEnd(); // END The Triangle Strip } glEndList(); // END The List free(last); // Free The Old Vertices Array return drawlist; // Return The Display List } Here we're just loading the matrix with some values I've picked that I think look cool. Feel free to screw around with these and see what it looks like. :-) void initBezier(void) { mybezier.anchors[0][0] = makePoint(-0.75, -0.75, -0.50); // Set The Bezier Vertices mybezier.anchors[0][1] = makePoint(-0.25, -0.75, 0.00); mybezier.anchors[0][2] = makePoint( 0.25, -0.75, 0.00); mybezier.anchors[0][3] = makePoint( 0.75, -0.75, -0.50); mybezier.anchors[1][0] = makePoint(-0.75, -0.25, -0.75); mybezier.anchors[1][1] = makePoint(-0.25, -0.25, 0.50); mybezier.anchors[1][2] = makePoint( 0.25, -0.25, 0.50); mybezier.anchors[1][3] = makePoint( 0.75, -0.25, -0.75); mybezier.anchors[2][0] = makePoint(-0.75, 0.25, 0.00); mybezier.anchors[2][1] = makePoint(-0.25, 0.25, -0.50); mybezier.anchors[2][2] = makePoint( 0.25, 0.25, -0.50); mybezier.anchors[2][3] = makePoint( 0.75, 0.25, 0.00); mybezier.anchors[3][0] = makePoint(-0.75, 0.75, -0.50); mybezier.anchors[3][1] = makePoint(-0.25, 0.75, -1.00); mybezier.anchors[3][2] = makePoint( 0.25, 0.75, -1.00); mybezier.anchors[3][3] = makePoint( 0.75, 0.75, -0.50); mybezier.dlBPatch = NULL; // Go Ahead And Initialize This To NULL } This is basically just an optimized routine to load a single bitmap. It can easily be used to load an array of em just by putting it in a simple loop. // Load Bitmaps And Convert To Textures BOOL LoadGLTexture(GLuint *texPntr, char* name) { BOOL success = FALSE; AUX_RGBImageRec *TextureImage = NULL; glGenTextures(1, texPntr); // Generate 1 Texture FILE* test=NULL; TextureImage = NULL; test = fopen(name, "r"); // Test To See If The File Exists if (test != NULL) { // If It Does fclose(test); // Close The File TextureImage = auxDIBImageLoad(name); // And Load The Texture } if (TextureImage != NULL) { // If It Loaded success = TRUE; // Typical Texture Generation Using Data From The Bitmap glBindTexture(GL_TEXTURE_2D, *texPntr); glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage->sizeX, TextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage->data); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); } if (TextureImage->data) free(TextureImage->data); return success; } Just adding the patch initialization here. You would do this whenever you create a patch. Again, this might be a cool place to use C++ (bezier class?). int InitGL(GLvoid) // All Setup For OpenGL Goes Here { glEnable(GL_TEXTURE_2D); // Enable Texture Mapping glShadeModel(GL_SMOOTH); // Enable Smooth Shading glClearColor(0.05f, 0.05f, 0.05f, 0.5f); // Black Background glClearDepth(1.0f); // Depth Buffer Setup glEnable(GL_DEPTH_TEST); // Enables Depth Testing glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations initBezier(); // Initialize the Bezier's Control Grid ( NEW ) LoadGLTexture(&(mybezier.texture), "./Data/NeHe.bmp"); // Load The Texture ( NEW ) mybezier.dlBPatch = genBezier(mybezier, divs); // Generate The Patch ( NEW ) return TRUE; // Initialization Went OK } First call the bezier's display list. Then (if the outlines are on) draw the lines connecting the control points. You can toggle these by pressing SPACE. int DrawGLScene(GLvoid) { // Here's Where We Do All The Drawing int i, j; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer glLoadIdentity(); // Reset The Current Modelview Matrix glTranslatef(0.0f,0.0f,-4.0f); // Move Left 1.5 Units And Into The Screen 6.0 glRotatef(-75.0f,1.0f,0.0f,0.0f); glRotatef(rotz,0.0f,0.0f,1.0f); // Rotate The Triangle On The Z-Axis glCallList(mybezier.dlBPatch); // Call The Bezier's Display List // This Need Only Be Updated When The Patch Changes if (showCPoints) { // If Drawing The Grid Is Toggled On glDisable(GL_TEXTURE_2D); glColor3f(1.0f,0.0f,0.0f); for(i=0;i<4;i++) { // Draw The Horizontal Lines glBegin(GL_LINE_STRIP); for(j=0;j<4;j++) glVertex3d(mybezier.anchors[i][j].x, mybezier.anchors[i][j].y, mybezier.anchors[i][j].z); glEnd(); } for(i=0;i<4;i++) { // Draw The Vertical Lines glBegin(GL_LINE_STRIP); for(j=0;j<4;j++) glVertex3d(mybezier.anchors[j][i].x, mybezier.anchors[j][i].y, mybezier.anchors[j][i].z); glEnd(); } glColor3f(1.0f,1.0f,1.0f); glEnable(GL_TEXTURE_2D); } return TRUE; // Keep Going } This function contains some modified code to make your projects more compatable. It doesn't have anything to do with Bezier curves, but it does fix a problem with switching back the resolution after fullscreen mode with some video cards (including mine, a crappy old ATI Rage PRO, and a few others). I hope, you'll use this from now on so me and others with similar cards can view your cool examples GL code properly. To make these modifications make the changes in KillGLWindow(), make sure and define DMsaved, and make the one line change in CreateGLWindow() (it's marked). GLvoid KillGLWindow(GLvoid) // Properly Kill The Window { if (fullscreen) // Are We In Fullscreen Mode? { if (!ChangeDisplaySettings(NULL,CDS_TEST)) { // If The Shortcut Doesn't Work ( NEW ) ChangeDisplaySettings(NULL,CDS_RESET); // Do It Anyway (To Get The Values Out Of The Registry) ( NEW ) ChangeDisplaySettings(&DMsaved,CDS_RESET); // Change It To The Saved Settings ( NEW ) } else { ChangeDisplaySettings(NULL,CDS_RESET); // If It Works, Go Right Ahead ( NEW ) } ShowCursor(TRUE); // Show Mouse Pointer } if (hRC) // Do We Have A Rendering Context? { if (!wglMakeCurrent(NULL,NULL)) // Are We Able To Release The DC And RC Contexts? { MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } if (!wglDeleteContext(hRC)) // Are We Able To Delete The RC? { MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } hRC=NULL; // Set RC To NULL } if (hDC && !ReleaseDC(hWnd,hDC)) // Are We Able To Release The DC { MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hDC=NULL; // Set DC To NULL } if (hWnd && !DestroyWindow(hWnd)) // Are We Able To Destroy The Window? { MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hWnd=NULL; // Set hWnd To NULL } if (!UnregisterClass("OpenGL",hInstance)) // Are We Able To Unregister Class { MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hInstance=NULL; // Set hInstance To NULL } } Just added the EnumDisplaySettings() command here to save the old display settings. (part of the old graphics card fix). // This Code Creates Our OpenGL Window. Parameters Are: * // title - Title To Appear At The Top Of The Window * // width - Width Of The GL Window Or Fullscreen Mode * // height - Height Of The GL Window Or Fullscreen Mode * // bits - Number Of Bits To Use For Color (8/16/24/32) * // fullscreenflag - Use Fullscreen Mode (TRUE) Or Windowed Mode (FALSE) */ BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag) { GLuint PixelFormat; // Holds The Results After Searching For A Match WNDCLASS wc; // Windows Class Structure DWORD dwExStyle; // Window Extended Style DWORD dwStyle; // Window Style RECT WindowRect; // Grabs Rectangle Upper Left / Lower Right Values WindowRect.left=(long)0; // Set Left Value To 0 WindowRect.right=(long)width; // Set Right Value To Requested Width WindowRect.top=(long)0; // Set Top Value To 0 WindowRect.bottom=(long)height; // Set Bottom Value To Requested Height fullscreen=fullscreenflag; // Set The Global Fullscreen Flag hInstance = GetModuleHandle(NULL); // Grab An Instance For Our Window wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // Redraw On Size, And Own DC For Window wc.lpfnWndProc = (WNDPROC) WndProc; // WndProc Handles Messages wc.cbClsExtra = 0; // No Extra Window Data wc.cbWndExtra = 0; // No Extra Window Data wc.hInstance = hInstance; // Set The Instance wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Load The Default Icon wc.hCursor = LoadCursor(NULL, IDC_ARROW); // Load The Arrow Pointer wc.hbrBackground = NULL; // No Background Required For GL wc.lpszMenuName = NULL; // We Don't Want A Menu wc.lpszClassName = "OpenGL"; // Set The Class Name EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &DMsaved); // Save The Current Display State ( NEW ) if (fullscreen) // Attempt Fullscreen Mode? { DEVMODE dmScreenSettings; // Device Mode memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); // Makes Sure Memory's Cleared dmScreenSettings.dmSize=sizeof(dmScreenSettings); // Size Of The Devmode Structure dmScreenSettings.dmPelsWidth = width; // Selected Screen Width dmScreenSettings.dmPelsHeight = height; // Selected Screen Height dmScreenSettings.dmBitsPerPel = bits; // Selected Bits Per Pixel dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT; ... Code Cut To Save Space (No Further Changes To This Function) ... return TRUE; // Success } All I did here was add commands to rotate the patch, raise/lower the resolution, and toggle the control lines. int WINAPI WinMain( HINSTANCE hInstance, // Instance HINSTANCE hPrevInstance, // Previous Instance LPSTR lpCmdLine, // Command Line Parameters int nCmdShow) // Window Show State { MSG msg; // Windows Message Structure BOOL done=FALSE; // Bool Variable To Exit Loop // Ask The User Which Screen Mode They Prefer if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; // Windowed Mode } // Create Our OpenGL Window if (!CreateGLWindow("NeHe's Solid Object Tutorial",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } while(!done) // Loop That Runs While done=FALSE { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting? { if (msg.message==WM_QUIT) // Have We Received A Quit Message? { done=TRUE; // If So done=TRUE } else // If Not, Deal With Window Messages { TranslateMessage(&msg); // Translate The Message DispatchMessage(&msg); // Dispatch The Message } } else // If There Are No Messages { // Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene() if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Active? Was There A Quit Received? { done=TRUE; // ESC or DrawGLScene Signalled A Quit } else // Not Time To Quit, Update Screen { SwapBuffers(hDC); // Swap Buffers (Double Buffering) } if (keys[VK_LEFT]) rotz -= 0.8f; // Rotate Left ( NEW ) if (keys[VK_RIGHT]) rotz += 0.8f; // Rotate Right ( NEW ) if (keys[VK_UP]) { // Resolution Up ( NEW ) divs++; mybezier.dlBPatch = genBezier(mybezier, divs); // Update The Patch keys[VK_UP] = FALSE; } if (keys[VK_DOWN] && divs > 1) { // Resolution Down ( NEW ) divs--; mybezier.dlBPatch = genBezier(mybezier, divs); // Update The Patch keys[VK_DOWN] = FALSE; } if (keys[VK_SPACE]) { // SPACE Toggles showCPoints ( NEW ) showCPoints = !showCPoints; keys[VK_SPACE] = FALSE; } if (keys[VK_F1]) // Is F1 Being Pressed? { keys[VK_F1]=FALSE; // If So Make Key FALSE KillGLWindow(); // Kill Our Current Window fullscreen=!fullscreen; // Toggle Fullscreen / Windowed Mode // Recreate Our OpenGL Window if (!CreateGLWindow("NeHe's Solid Object Tutorial",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } } } } // Shutdown KillGLWindow(); // Kill The Window return (msg.wParam); // Exit The Program } Well, I hope this tutorial has been enlightening and you all now love Bezier curves as much as I do ;-). If you like this tutorial I may write another one on NURBS curves if anyone's interested. Please e-mail me and let me know what you thought of this tutorial. About The Author: David Nikdel is currently 18 and a senior at Bartow Senior High School. His current projects include a research paper on curved surfaces in 3D graphics, an OpenGL based game called Blazing Sands and being lazy. His hobbies include programming, football, and paintballing. He will (hopefully) be a freshman at Georgia Tech next year. David Nikdel Jeff Molofee (NeHe) * DOWNLOAD Visual C++ Code For This Lesson. * DOWNLOAD Borland C++ Builder 6 Code For This Lesson. ( Conversion by Christian Kindahl )
|