unit GLUtils; interface uses MemoryBuffer, Mat4, GLTypes, BrowserConsole, WebGL, JS, Types, SysUtils; type TShader = class public constructor Create (context: TJSWebGLRenderingContext; vertexShaderSource, fragmentShaderSource: string); procedure Compile; procedure Link; procedure Use; function GetAttribLocation (name: string): GLint; procedure BindAttribLocation (index: GLuint; name: string); procedure SetUniformMat4 (name: string; value: TMat4); procedure SetUniformVec3 (name: string; value: TVec3); procedure SetUniformFloat (name: string; value: GLfloat); private gl: TJSWebGLRenderingContext; vertexShader: TJSWebGLShader; fragmentShader: TJSWebGLShader; programID: TJSWebGLProgram; function GetUniformLocation (name: string): TJSWebGLUniformLocation; function CreateShader (theType: GLenum; source: string): TJSWebGLShader; end; type TModelData = record verticies: TJSFloat32Array; // GLfloat // NOTE: it's not clear if WebGL supports GLuint // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawElements indicies: TJSUint16Array; // GLushort floatsPerVertex: integer; end; const kModelVertexFloats = 3 + 2 + 3; type TModelVertex = record pos: TVec3; texCoord: TVec2; normal: TVec3; end; procedure ModelVertexAddToBuffer(vertex: TModelVertex; buffer: TMemoryBuffer); procedure ModelVertexAddToArray (vertex: TModelVertex; list: TJSArray); type TModel = class public constructor Create(context: TJSWebGLRenderingContext; modelData: TModelData); overload; procedure Draw; private gl: TJSWebGLRenderingContext; data: TModelData; vertexBuffer: TJSWebGLBuffer; indexBuffer: TJSWebGLBuffer; //elementCount: integer; procedure EnableAttributes; procedure Load; end; function LoadOBJFile (text: TJSString): TModelData; function GLSizeof(glType: NativeInt): integer; procedure GLFatal (gl: TJSWebGLRenderingContext; messageString: string = 'Fatal OpenGL error'); implementation {=============================================} {@! ___Utilities___ } {=============================================} procedure Fatal (messageString: string); overload; begin writeln('*** FATAL: ', messageString); raise Exception.Create('FATAL'); end; // TODO: toll free bridge to FPC strings {procedure Fatal (messageString: TJSString); overload; begin writeln('*** FATAL: ', messageString); raise Exception.Create('FATAL'); end;} procedure GLFatal (gl: TJSWebGLRenderingContext; messageString: string = 'Fatal OpenGL error'); var error: integer; begin error := gl.getError(); if error <> TJSWebGLRenderingContext.NO_ERROR then begin // TODO: case doesn't work? case error of TJSWebGLRenderingContext.INVALID_VALUE: messageString := messageString+' (GL_INVALID_VALUE)'; TJSWebGLRenderingContext.INVALID_OPERATION: messageString := messageString+' (GL_INVALID_OPERATION)'; TJSWebGLRenderingContext.INVALID_ENUM: messageString := messageString+' (GL_INVALID_ENUM)'; otherwise messageString := messageString+' '+IntToStr(error); end; Fatal(messageString); end; end; function GLSizeof(glType: NativeInt): integer; begin case glType of TJSWebGLRenderingContext.UNSIGNED_BYTE, TJSWebGLRenderingContext.BYTE: result := 1; TJSWebGLRenderingContext.SHORT, TJSWebGLRenderingContext.UNSIGNED_SHORT: result := 2; TJSWebGLRenderingContext.INT, TJSWebGLRenderingContext.UNSIGNED_INT: result := 4; TJSWebGLRenderingContext.FLOAT: result := 4; otherwise Fatal('GLSizeof type is invalid.'); end; end; {=============================================} {@! ___Model___ } {=============================================} procedure ModelVertexAddToBuffer(vertex: TModelVertex; buffer: TMemoryBuffer); begin buffer.AddFloats(kModelVertexFloats, [ vertex.pos.x, vertex.pos.y, vertex.pos.z, vertex.texCoord.x, vertex.texCoord.y, vertex.normal.x, vertex.normal.y, vertex.normal.z ]); end; procedure ModelVertexAddToArray (vertex: TModelVertex; list: TJSArray); begin list.push(vertex.pos.x); list.push(vertex.pos.y); list.push(vertex.pos.z); list.push(vertex.texCoord.x); list.push(vertex.texCoord.y); list.push(vertex.normal.x); list.push(vertex.normal.y); list.push(vertex.normal.z); end; constructor TModel.Create(context: TJSWebGLRenderingContext; modelData: TModelData); begin gl := context; data := modelData; Load; end; procedure TModel.Draw; begin gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); EnableAttributes; gl.drawElements(gl.TRIANGLES, data.indicies.length, gl.UNSIGNED_SHORT, 0); gl.bindBuffer(gl.ARRAY_BUFFER, nil); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, nil); end; procedure TModel.EnableAttributes; var offset: integer; stride: integer; begin // NOTE: we don't have VAO's yet so we need to enable vertex attributes for shader // before every draw call (unless the array buffer hasn't changed between calls) offset := 0; stride := data.floatsPerVertex * GLSizeof(TJSWebGLRenderingContext.FLOAT); // position gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, offset); offset += GLSizeof(TJSWebGLRenderingContext.FLOAT) * 3; // texture gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, offset); offset += GLSizeof(TJSWebGLRenderingContext.FLOAT) * 2; // normal gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 3, gl.FLOAT, false, stride, offset); offset += GLSizeof(TJSWebGLRenderingContext.FLOAT) * 3; end; procedure TModel.Load; begin indexBuffer := gl.createBuffer; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data.indicies, gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, nil); vertexBuffer := gl.createBuffer; gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, data.verticies, gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, nil); end; type TOBJVertex = record position: TVec3; textureIndex: integer; normalIndex: integer; end; function ProcessFace (verticies: TJSArray; indices: TJSArray; face: TStringDynArray): TOBJVertex; var index: integer; vertex: TOBJVertex; begin index := StrToInt(face[0]) - 1; vertex := TOBJVertex(verticies[index]); // NOTE: see TOBJData // index can't exceed GLushort if index > 65536 then Fatal('overflowed indices array'); if face[1] <> '' then vertex.textureIndex := StrToInt(face[1]) - 1 else vertex.textureIndex := -1; if face[2] <> '' then vertex.normalIndex := StrToInt(face[2]) - 1 else vertex.normalIndex := -1; indices.push(index); verticies[index] := vertex; result := vertex; end; function LoadOBJFile (text: TJSString): TModelData; const kLineEnding = #10; kSpace = ' '; // what code is space? var lines: TStringDynArray; parts: TStringDynArray; indices: TJSArray; positions: TJSArray; normals: TJSArray; textures: TJSArray; verticies: TJSArray; mesh: TJSFloat32Array; i: integer; line: TJSString; vertex: TOBJVertex; vertexIndex: integer; data: TModelData; pos: TVec3; texCoord: TVec2; normal: TVec3; begin positions := TJSArray.new; normals := TJSArray.new; textures := TJSArray.new; indices := TJSArray.new; verticies := TJSArray.new; lines := text.split(kLineEnding); for i := 0 to high(lines) do begin line := TJSString(lines[i]); parts := line.split(kSpace); if line.startsWith('v ') then begin pos := V3(StrToFloat(parts[1]), StrToFloat(parts[2]), StrToFloat(parts[3])); positions.push(pos); // add new vertex vertex.position := pos; vertex.textureIndex := -1; vertex.normalIndex := -1; verticies.push(vertex); end else if line.startsWith('vn ') then begin normals.push(V3(StrToFloat(parts[1]), StrToFloat(parts[2]), StrToFloat(parts[3]))); end else if line.startsWith('vt ') then begin textures.push(V2(StrToFloat(parts[1]), 1 - StrToFloat(parts[2]))); end else if line.startsWith('f ') then begin ProcessFace(verticies, indices, TJSString(parts[1]).split('/')); ProcessFace(verticies, indices, TJSString(parts[2]).split('/')); ProcessFace(verticies, indices, TJSString(parts[3]).split('/')); end; end; // vec3 (position) + vec2 (texCoord) + vec3 (normal) data.floatsPerVertex := kModelVertexFloats; mesh := TJSFloat32Array.New(data.floatsPerVertex * verticies.length); for i := 0 to verticies.length - 1 do begin vertex := TOBJVertex(verticies[i]); vertexIndex := i * data.floatsPerVertex; // position pos := TVec3(positions[i]); mesh[vertexIndex + 0] := pos.x; mesh[vertexIndex + 1] := pos.y; mesh[vertexIndex + 2] := pos.z; // texture if vertex.textureIndex <> -1 then begin texCoord := TVec2(textures[vertex.textureIndex]); mesh[vertexIndex + 3] := texCoord.x; mesh[vertexIndex + 4] := texCoord.y; end else begin mesh[vertexIndex + 3] := 0; mesh[vertexIndex + 4] := 0; end; // normal if vertex.normalIndex <> -1 then begin normal := TVec3(normals[vertex.normalIndex]); mesh[vertexIndex + 5] := normal.x; mesh[vertexIndex + 6] := normal.y; mesh[vertexIndex + 7] := normal.z; end; end; //writeln('floats: ', mesh.length); //writeln('positions:', positions.length); //writeln('indices:', indices.length); data.verticies := mesh; data.indicies := TJSUint16Array.New(TJSObject(indices)); result := data; end; {=============================================} {@! ___Shader___ } {=============================================} function TShader.GetUniformLocation (name: string): TJSWebGLUniformLocation; begin // TODO: cache these. how do we use dictionarys from JS in Pascal? result := gl.getUniformLocation(programID, name); GLFatal(gl, 'gl.getUniformLocation'); end; procedure TShader.SetUniformFloat (name: string; value: GLfloat); begin gl.uniform1f(GetUniformLocation(name), value); GLFatal(gl, 'gl.uniform1f'); end; procedure TShader.SetUniformVec3 (name: string; value: TVec3); begin //gl.uniform3fv(GetUniformLocation(name), ToFloats(value)); gl.uniform3f(GetUniformLocation(name), value.x, value.y, value.z); GLFatal(gl, 'gl.uniform3fv'); end; procedure TShader.SetUniformMat4 (name: string; value: TMat4); var list: TJSFloat32List; begin // TODO: fix mat4 to use flat arrays list := TJSFloat32List(value.CopyList); gl.uniformMatrix4fv(GetUniformLocation(name), false, list); GLFatal(gl, 'gl.uniformMatrix4fv'); end; function TShader.GetAttribLocation (name: string): GLint; begin result := gl.getAttribLocation(programID, name); end; procedure TShader.BindAttribLocation (index: GLuint; name: string); begin gl.bindAttribLocation(programID, index, name); //GLFatal('glBindAttribLocation '+IntToStr(index)+':'+name); end; constructor TShader.Create (context: TJSWebGLRenderingContext; vertexShaderSource, fragmentShaderSource: string); begin gl := context; vertexShader := CreateShader(gl.VERTEX_SHADER, vertexShaderSource); fragmentShader := CreateShader(gl.FRAGMENT_SHADER, fragmentShaderSource); end; function TShader.CreateShader(theType: GLenum; source: string): TJSWebGLShader; begin Result := gl.createShader(theType); if Result = nil then Fatal('create shader failed'); gl.shaderSource(Result, source); gl.compileShader(Result); if gl.getShaderParameter(Result, gl.COMPILE_STATUS) then begin //writeln('loaded shader ', theType); exit; end else begin Fatal(gl.getShaderInfoLog(Result)); //gl.deleteShader(shader); end; end; procedure TShader.Compile; begin programID := gl.createProgram; gl.attachShader(programID, vertexShader); gl.attachShader(programID, fragmentShader); end; procedure TShader.Link; begin gl.linkProgram(programID); if not gl.getProgramParameter(programID, gl.LINK_STATUS) then begin Fatal(gl.getProgramInfoLog(programID)); //gl.deleteProgram(programID); end; end; procedure TShader.Use; begin gl.useProgram(programID); end; end.