# JohnBrosz

## 08022008

Filed in: CSharp.08022008 · Modified on : Thu, 21 Feb 08

Shaders in OpenGL (with TAO and GLFW)

Here's an example of using GLSL shaders in C#. I've made the code fairly big so you can figure out how to do most of what you're likely to be wanting to do.

First of all, the shaders:

```uniform float fisheye;	// decide whether we do gl projection or custom fisheye projection
uniform bool textured;	// variable used to determine if an object is textured or not

// three vectors used for lighting calculations
vec4 Ambient;
vec4 Diffuse;
vec4 Specular;

// lighting contribution from a point light source - includes attenuation
void pointLight(in int i, in vec3 normal, in vec3 eye, in vec3 ecPosition3)
{
float nDotVP;       // normal . light direction
float nDotHV;       // normal . light half vector
float pf;           // power factor
float attenuation;  // computed attenuation factor
float d;            // distance from surface to light source
vec3  VP;           // direction from surface to light position
vec3  halfVector;   // direction of maximum highlights

// Compute vector from surface to light position
VP = gl_LightSource[i].position - ecPosition3;

// Compute distance between surface and light position
d = length(VP);

// Normalize the vector from surface to light position
VP = normalize(VP);

// Compute attenuation
attenuation = 1.0 / (gl_LightSource[i].constantAttenuation +
gl_LightSource[i].linearAttenuation * d +
gl_LightSource[i].quadraticAttenuation * d * d);

halfVector = normalize(VP + eye);

nDotVP = max(0.0, dot(normal, VP));
nDotHV = max(0.0, dot(normal, halfVector));

if (nDotVP == 0.0)
pf = 0.0;
else
pf = pow(nDotHV, gl_FrontMaterial.shininess);

Ambient  += gl_LightSource[i].ambient * attenuation;
Diffuse  += gl_LightSource[i].diffuse * nDotVP * attenuation;
Specular += gl_LightSource[i].specular * pf * attenuation;
}

// lighting contribution from a directional light
void directionalLight(in int i, in vec3 normal)
{
float nDotVP;         // normal . light direction
float nDotHV;         // normal . light half vector
float pf;             // power factor

nDotVP = max(0.0, dot( normal, normalize(vec3(gl_LightSource[i].position)) ));
nDotHV = max(0.0, dot( normal, vec3(gl_LightSource[i].halfVector) ));

if (nDotVP == 0.0)
pf = 0.0;
else
pf = pow(nDotHV, gl_FrontMaterial.shininess);

Ambient  += gl_LightSource[i].ambient;
Diffuse  += gl_LightSource[i].diffuse * nDotVP;
Specular += gl_LightSource[i].specular * pf;
}

// lighting code . . . adds contributions from OpenGL lights
void flight(in vec3 normal, in vec4 ecPosition)
{
vec4 color;
vec3 ecPosition3;
vec3 eye = vec3(0.0,0.0,1.0);

ecPosition3 = (vec3 (ecPosition)) / ecPosition.w;

// Clear the light intensity accumulators
Ambient  = vec4 (0.0);
Diffuse  = vec4 (0.0);
Specular = vec4 (0.0);

pointLight(0, normal, eye, ecPosition3);
directionalLight(1, normal);

// color = gl_FrontLightModelProduct.sceneColor + Ambient  * gl_FrontMaterial.ambient + Diffuse  * gl_FrontMaterial.diffuse;    // for normal gl materials
color = gl_FrontLightModelProduct.sceneColor + (Ambient + Diffuse) * gl_Color; // gl color material
color += Specular * gl_FrontMaterial.specular;
color = clamp( color, 0.0, 1.0 );
gl_FrontColor = color;
}

void main (void)
{
// Eye-coordinate position of vertex, needed in various calculations
vec4 ecPosition = gl_ModelViewMatrix * gl_Vertex;

if (fisheye <= 0.0)
gl_Position = ftransform(); // ftransform() is almost the same as gl_ModelViewProjectionMatrix * gl_Vertex;
else
{
vec3 eye3 = vec3(ecPosition) * (1.0 / ecPosition.w);

// change to polar coordinates
float radius = 50.0;  // fisheye hemisphere radius
vec3 p = eye3;
vec3 pn = p;
float d = length(p);
pn = normalize(p);
d = d / radius;
float u = atan2(pn.y,pn.x);
float v = 2.0 * acos(-pn.z) / 3.141529265358979;

// change polar to cartesian coordinates on circle (with depth)
gl_Position.x = cos(u) * v;
gl_Position.y = sin(u) * v;
gl_Position.z = d;// * -1.0 * abs(p.z) / p.z;
gl_Position.w = 1.0;
}

vec3 normal = gl_NormalMatrix * gl_Normal;
normal = normalize(normal);

// do lighting calculation
flight(normal, ecPosition);

// pass on the texture coordinate
gl_TexCoord[0] = gl_MultiTexCoord0;
}
```

Okay, so this is the vertex shader. As you can see most of the code is necessary for directional and point lights. This code actually started with auto-generated code created by 3DLabs GLSL ShaderGen (see http://developer.3dlabs.com). I've added to uniform variables, one to control the type of projection performed, the other to assist in determining whether texture mapping is desired or not. The fisheye variable should be bool but this was causing problems (I could not retrieve a proper uniform variable reference for it in the application) so I made it a float and it magically worked.

Now, onto the fragment shader . . .

```uniform sampler2D texUnit0;
uniform float fisheye;
uniform bool textured;

void main (void)
{
vec4 color = gl_Color;
if (textured)
color = color * texture2D(texUnit0, gl_TexCoord[0].xy);
gl_FragColor = color;
}
```

Very straightforward, nothing exciting.

Now onto the application itself. For this application I added references to Microsoft.DirectX, Tao.Glfw, and Tao.OpenGL. I've also added the two shader files to the project and included them in the executable as an embedded resource (this is a file property under build action). I've also included a texture (water.bmp) as another embedded resource in the project.

```using System;
using Tao.OpenGl;
using Tao.Glfw;
using System.Windows.Forms;
using Microsoft.DirectX;

namespace CSharpGLBase
{
static class Program
{
static bool exit = false;
static bool verbose = true; // causes program to spew extra info to console
static string windowTitle;
const int screenWidth = 1024;
const int screenHeight = 768;
static float angle = 0; // controls rotation of cube
static float xTrn=0, zTrn=0; // controls translation of camera
static int TexID = 0;
static Glu.GLUquadric SphereID;
static int vs_object, fs_object, program; // shader variables
static int shaderChoice = 0;
static int fisheyeUniformVar = 0;
static int texturedUniformVar = 0;
static bool doFisheye = false;
```
I'm going to insert little comments here and there throughout this large file as you see here. Notice that everything is static as we're not actually instantiating this object.
```        [STAThread]
static void Main()
{
double t, t0, fps;
int frames=0;

initGLFW();

t0 = Glfw.glfwGetTime();
while (!exit)
{
Glfw.glfwPollEvents();

if (frames++ > 40)
{
t = Glfw.glfwGetTime();
fps = (double)frames / (t - t0);
Glfw.glfwSetWindowTitle(windowTitle + "     FPS: " + fps.ToString("#.##"));
t0 = t;
frames = 0;
} // end if

GLPaint();

Glfw.glfwSwapBuffers();
} // end while

Glfw.glfwTerminate();
}
```
That's the main event loop. Basically we do the GLFW initialization, the OpenGL initialization, then we loop, polling events, calculating FPS, and rendering.
```        static void initGLFW()
{
Glfw.glfwInit();
Glfw.glfwOpenWindow(1024, 768, 8, 8, 8, 8, 16, 0, Glfw.GLFW_WINDOW);
windowTitle = " OpenGL Version " + Gl.glGetString(Gl.GL_VERSION) + " on " + Gl.glGetString(Gl.GL_RENDERER);
Glfw.glfwSetWindowTitle(windowTitle);

Glfw.glfwEnable(Glfw.GLFW_KEY_REPEAT); // allow for repeating keys when key held down
Glfw.glfwSwapInterval(1); // wait for at least 1 vertical refresh before swapping buffers (0 will remove any vsync)

// do OpenGL initialization stuff
GLInit();

// set callback functions for various and sundry events
Glfw.GLFWwindowsizefun ResizeCallback = new Glfw.GLFWwindowsizefun(Resize);
Glfw.GLFWwindowclosefun CloseCallback = new Glfw.GLFWwindowclosefun(Close);
Glfw.GLFWkeyfun KeyCallback = new Glfw.GLFWkeyfun(KeyHandler);
Glfw.GLFWmousebuttonfun MouseCallback = new Glfw.GLFWmousebuttonfun(MouseButtonHandler);
Glfw.GLFWmouseposfun MouseMoveCallback = new Glfw.GLFWmouseposfun(MouseMoveHandler);
Glfw.GLFWmousewheelfun MouseWheelCallback = new Glfw.GLFWmousewheelfun(MouseWheelHandler);
Glfw.glfwSetWindowSizeCallback(ResizeCallback);
Glfw.glfwSetWindowCloseCallback(CloseCallback);
Glfw.glfwSetKeyCallback(KeyCallback);
Glfw.glfwSetMouseButtonCallback(MouseCallback);
Glfw.glfwSetMousePosCallback(MouseMoveCallback);
Glfw.glfwSetMouseWheelCallback(MouseWheelCallback);
}
```
Nothing very exciting, just setting callback functions for event handling.
```        static void Resize(int w, int h)
{
if (h == 0) h = 1;
double ratio = 1.0 * w / h;
Gl.glMatrixMode(Gl.GL_PROJECTION);
Gl.glLoadIdentity();
Gl.glViewport(0, 0, w, h);
Glu.gluPerspective(45.0, ratio, 1.0, 64.0);
Gl.glMatrixMode(Gl.GL_MODELVIEW);
}

static int Close()
{
exit = true;
return 0;
}

static void GLInit()
{
Gl.glShadeModel(Gl.GL_SMOOTH);                                      // Enable Smooth Shading
Gl.glClearColor(0, 0, 0, 0);                                     // Black Background
Gl.glEnable(Gl.GL_DEPTH_TEST);                                      // Enables Depth Testing
Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST);         // Really Nice Perspective Calculations

// load texture
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
System.IO.Stream stream = assembly.GetManifestResourceStream("CSharpGLBase.water.bmp");
TexID = Helpers.OpenGLUtilities.LoadTexture(stream);
stream.Close();
stream.Dispose();

// create sphere
SphereID = Glu.gluNewQuadric();

// turn on the lights
float[] mat_shininess = { 50 };
float[] light1_position = { -1, 2, 3, 0 };
float[] white_light = {1, 1, 1, 1};
float[] lmodel_ambient = {0.1f, 0.1f, 0.1f, 1};

Gl.glShadeModel( Gl.GL_SMOOTH );
//Gl.glEnable(Gl.GL_CULL_FACE);
Gl.glMaterialfv(Gl.GL_FRONT, Gl.GL_SHININESS, mat_shininess);
//Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_POSITION, light0_position); // note we don't need light0 now, this is done in GLPaint()
Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, white_light);
Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, white_light);
Gl.glLightfv(Gl.GL_LIGHT1, Gl.GL_POSITION, light1_position);
Gl.glLightfv(Gl.GL_LIGHT1, Gl.GL_DIFFUSE, white_light);
Gl.glLightfv(Gl.GL_LIGHT1, Gl.GL_SPECULAR, white_light);
Gl.glLightModelfv(Gl.GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
// put in some default colors
Gl.glMaterialfv(Gl.GL_FRONT_AND_BACK, Gl.GL_AMBIENT_AND_DIFFUSE, white_light);
Gl.glMaterialfv(Gl.GL_FRONT_AND_BACK, Gl.GL_SPECULAR, white_light);
Gl.glEnable(Gl.GL_LIGHTING);
Gl.glEnable(Gl.GL_LIGHT0);
Gl.glEnable(Gl.GL_LIGHT1);
// enable color material - note for some reason OpenGL does not automatically place the
// colors in the gl_Front_Material variable so be careful to use gl_Color in the shader instead.
Gl.glColorMaterial(Gl.GL_FRONT_AND_BACK, Gl.GL_AMBIENT_AND_DIFFUSE);
Gl.glEnable(Gl.GL_COLOR_MATERIAL);

#region shader initialization
stream = assembly.GetManifestResourceStream("CSharpGLBase.default.vert");
vs_object = Helpers.OpenGLUtilities.createAndCompileShader(stream,Gl.GL_VERTEX_SHADER);
stream.Close();
stream.Dispose();
stream = assembly.GetManifestResourceStream("CSharpGLBase.default.frag");
fs_object = Helpers.OpenGLUtilities.createAndCompileShader(stream,Gl.GL_FRAGMENT_SHADER);
stream.Close();
stream.Dispose();

program = Helpers.OpenGLUtilities.createAttachLinkShaderProgram(vs_object, fs_object);

// get shader variables
fisheyeUniformVar = Gl.glGetUniformLocation(program, "fisheye");
texturedUniformVar = Gl.glGetUniformLocation(program, "textured");

#endregion shader initialization
}
```
Ok, here's the start of the meat and potatoes. First of all I load a texture. The details of this are left out, eventually you'll seem them in the next file (OpenGLUtilities). All that stuff with System.Reflection.Assembly is necessary to get at the embedded resource files. Then we go on to OpenGL lighting code. Be sure to note the comment about COLOR_MATERIALS. Seems weird that OpenGL does not bother to copy over when this is enabled, but oh well. Then comes code for loading the shaders and the shader program. The details of these will be covered when we get to OpenGLUtilities. Last of all we get the shader variables (this is where the bool fisheye variable was causing problems so I turned it into a float).
```        static void GLPaint()
{
Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
Gl.glMatrixMode(Gl.GL_MODELVIEW);
Gl.glLoadIdentity();

switch (shaderChoice)
{
case 0:
Gl.glUseProgram(0);
break;
case 1:
Gl.glUseProgram(program);
break;
default:
throw new Exception("Invalid shaderChoice value");
}

Glu.gluLookAt(0.0f, 2.0f, 0.0f, 0.0f, 0.0f, 5.0f, 0.0f, 1.0f, 0.0f);
Gl.glTranslatef(xTrn, 0, zTrn + 5.0f);

float[] lpos = { -10, 1, -2, 1};
Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_POSITION, lpos);

// draw cube
Gl.glPushMatrix();
Gl.glRotatef(angle, 0.0f, 1.0f, 0.0f);
drawCube();
Gl.glPopMatrix();

Gl.glPushMatrix();
Gl.glUniform1i(texturedUniformVar, 0); // make the shader note that this is not shaded
Gl.glTranslatef(2, 0, 0);
Gl.glColor3f(1, 0, 0);
Glu.gluSphere(SphereID, 0.5, 10, 10);
Gl.glPopMatrix();

angle += 0.3f;
}
```
Code to draw basic geometry (a rotating cube and a gluSphere. I'm leaving out the drawCube() code as it's pretty basic. If you're interested download the code.
```        #region event handlers
static void KeyHandler(int key, int action)
{
// see if a key has actually been pressed
if (action != Glfw.GLFW_PRESS)
return;

const float speed = 0.1f;

switch (key)
{
case Glfw.GLFW_KEY_ESC:
exit = true;
break;
case Glfw.GLFW_KEY_LEFT:
xTrn += speed;
break;
case Glfw.GLFW_KEY_RIGHT:
xTrn -= speed;
break;
case Glfw.GLFW_KEY_UP:
zTrn += speed;
break;
case Glfw.GLFW_KEY_DOWN:
zTrn -= speed;
break;
case Glfw.GLFW_KEY_F1:
shaderChoice = 0;
break;
case Glfw.GLFW_KEY_F2:
shaderChoice = 1;
break;
case (int)'P':
if (doFisheye)
Gl.glUniform1f(fisheyeUniformVar, 0.0f);
else
Gl.glUniform1f(fisheyeUniformVar, 1.0f);
doFisheye = !doFisheye;
break;
} // end switch
} // end key handler

static void MouseButtonHandler(int mouseButton, int action)
{
if (action != Glfw.GLFW_PRESS)
return;

switch (mouseButton)
{
case Glfw.GLFW_MOUSE_BUTTON_LEFT:
Console.WriteLine("left mouse pressed");
break;
case Glfw.GLFW_MOUSE_BUTTON_RIGHT:
Console.WriteLine("right mouse pressed");
break;
case Glfw.GLFW_MOUSE_BUTTON_MIDDLE:
Console.WriteLine("middle mouse pressed");
break;
} // end switch
}

private static int oldMouseX=-1;
private static int oldMouseY=-1;
static void MouseMoveHandler(int mouseX, int mouseY)
{
oldMouseX = mouseX;
oldMouseY = mouseY;
}

static void MouseWheelHandler(int wheelpos)
{
Console.WriteLine("mouse wheel moved to " + wheelpos);
}
#endregion event handlers
} // end class
} // end namespace
```
And that's about it. Now last but not least is the my OpenGL Utilities class.
```using System;
using System.Drawing;
using Tao.OpenGl;

namespace Helpers
{
public class OpenGLUtilities
{
public static int LoadTexture(string path)
{
if (!System.IO.File.Exists(path))
throw new Exception("Helpers.OpenGLUtilities::LoadTexture - Error, file " + path + " does not exist.");
System.IO.Stream s = new System.IO.FileStream(path, System.IO.FileMode.Open);
return LoadTexture(s);
} // end load texture

public static int LoadTexture(System.IO.Stream texfile)
{
if (texfile == null)
throw new Exception("Helpers.OpenGLUtilities::LoadTexture - Error, null stream passed as texfile parameter.");

Bitmap[] textureImage = new Bitmap[1];
textureImage[0] = new Bitmap(texfile);
int[] texture = new int[1];

// Check For Errors, if bitmap's not found complain loudly
if (textureImage[0] != null)
{
Gl.glGenTextures(1, texture);  // Create The Texture
textureImage[0].RotateFlip(RotateFlipType.RotateNoneFlipY);     // Flip The Bitmap Along The Y-Axis
// Rectangle For Locking The Bitmap In Memory
Rectangle rectangle = new Rectangle(0, 0, textureImage[0].Width, textureImage[0].Height);
// Get The Bitmap's Pixel Data From The Locked Bitmap
System.Drawing.Imaging.BitmapData bitmapData = textureImage[0].LockBits(rectangle,
System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
// Typical Texture Generation Using Data From The Bitmap
Gl.glBindTexture(Gl.GL_TEXTURE_2D, texture[0]);
Gl.glTexImage2D(Gl.GL_TEXTURE_2D, 0, Gl.GL_RGB8, textureImage[0].Width, textureImage[0].Height, 0, Gl.GL_BGR,
Gl.GL_UNSIGNED_BYTE, bitmapData.Scan0);
Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_LINEAR);
Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_LINEAR);
// now remove the Bitmap from memory
if (textureImage[0] != null)
{
textureImage[0].UnlockBits(bitmapData);                     // Unlock The Pixel Data From Memory
textureImage[0].Dispose();                                  // Dispose The Bitmap
}
}
else
throw new Exception("Helpers.OpenGLUtilities::LoadTexture - Error in loading bitmap; stream return null bitmap.");
return texture[0];
}
```
So this is my way of loading texture files. Notice how .Net's pretty Bitmap class does all the heavy lifting for us. Occasionally the new Bitmap(texfile) line of code causes a freeze (it seems to wait forever for the IO). One day when this annoys me enough I'll figure out why. For now I just stop the program and restart.
```        public static int createAndCompileShader(System.IO.Stream shaderFileStream, int GLSHADERTYPE)
{
if (GLSHADERTYPE != Gl.GL_FRAGMENT_SHADER && GLSHADERTYPE != Gl.GL_VERTEX_SHADER)
throw new Exception("Helpers.OpenGLUtilities::createAndCompileShader - Invalid GLSHADERTYPE, use GL_FRAGMENT_SHADER or GL_VERTEX_SHADER");
int shader = Gl.glCreateShader(GLSHADERTYPE);

string[] shaderSource = loadTextFile(shaderFileStream);

Gl.glShaderSource(shader, shaderSource.Length, shaderSource, null);
Gl.glCompileShader(shader);

// now test for errors, I'm not sure which error testing is better so for now I'll do both
printInfoLog(shader);

int[] status = new int[1];
Gl.glGetShaderiv(shader, Gl.GL_COMPILE_STATUS, status);
if (status[0] != Gl.GL_TRUE)
{
int[] l = new int[1];
System.Text.StringBuilder log = new System.Text.StringBuilder(1024);
Gl.glGetShaderInfoLog(shader, 1024, l, log);
if (GLSHADERTYPE == Gl.GL_FRAGMENT_SHADER)
Console.Write("FRAGMENT ");
else
Console.Write("VERTEX ");
Console.WriteLine("SHADER LOG: \n" + log.ToString());
throw new Exception("Could not compile shader");
}
return shader;
}
```
Okay, so this loads a vertex or fragment shader for us. It's nice to have a function for this as the code to check the OpenGL logs is a bit messy. Also if you should ever happen to make a fragment shader that doesn't compile this you will notice that this code gives you the errors twice . . . I like redundancy, especially with error messages.
```        public static int createAttachLinkShaderProgram(int vShader, int fShader)
{
if (vShader <= 0 && fShader <= 0)
throw new Exception("Error, passing empty shaders to creaetAttachLinkShaderProgram");

int program = Gl.glCreateProgram();
if (vShader >= 0)
Gl.glAttachShader(program, vShader);
if (fShader >= 0)
Gl.glAttachShader(program, fShader);
Gl.glLinkProgram(program);

// test for errors - check program log
int[] status = new int[1];
Gl.glGetProgramiv(program, Gl.GL_LINK_STATUS, status);
if (status[0] != Gl.GL_TRUE)
{
int[] l = new int[1];
System.Text.StringBuilder log = new System.Text.StringBuilder(4024);
Gl.glGetProgramInfoLog(program, 4024, l, log);
Console.WriteLine("PROG LOG: " + log.ToString());

log = new System.Text.StringBuilder(4024);
Gl.glValidateProgram(program);
Gl.glGetProgramInfoLog(program, 4024, l, log);
Console.WriteLine("PROG LOG2: " + log.ToString());
throw new Exception("Problem in linking shaders");
} // end if

printInfoLog(program);
return program;
}
```
So this created a shader program object for you. If you only want a vertex or a fragment shader just pass a 0 to the one you don't want. Again this function is likely to throw redundant error messages.
```
/// <summary>
/// Loads a text file into a string array.  Each line of the text file
/// forms a string in the array.  Coments in // style are automatically
/// removed as these seem to causes GLSL compiler errors.
/// </summary>
/// <param name="textfile">stream of the file you wish to load</param>
/// <returns></returns>
public static string[] loadTextFile(System.IO.Stream textfile)
{
if (textfile == null)
throw new Exception("Error, null textfile stream to loadTextFile");
System.Collections.ArrayList list = new System.Collections.ArrayList();
System.IO.StreamReader reader = new System.IO.StreamReader(textfile);
while (!reader.EndOfStream)
{
string s = reader.ReadLine();
// filter out shader comments . . . these seem to cause compiler errors
int i = s.IndexOf("//");
if (i > 0 && i < s.Length)
s = s.Substring(0,i);
if (i != 0)
list.Add(s);
}
reader.Close();
reader.Dispose();
return (string[])list.ToArray(typeof(String));
}

} // end opengl utilities
} // end helpers namespace
```
Okay so we've ended with the last bit, code that loads a stream to a texture file into an array of strings. Tao OpenGL shaders need this array of strings to load shaders. The only little tricky thing I've done here is to automatically exclude the C++ style comments from the shaders. For some reason this style of comment was causing the GLSL shader grief.

That's it. One thing I really should have done is to do some sort of checking to ensure we're dealing with OpenGL 2.0+ to ensure we have proper shader support.

Here's a download of the entire project: C# OpenGL GLSL Base Code

Powered by PmWiki