mirror of
				https://gitlab.com/freepascal.org/fpc/pas2js.git
				synced 2025-10-31 16:11:23 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			464 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
			
		
		
	
	
			464 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
| 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.
 | 
