Using Two Shader Programs

This demonstration uses one shader program to colour four faces of a cube and another to display textures near a top corner of two of the faces. The idea behind the project is to be able to view from different angles photographs of two paintings near to each other on walls of different colours in order to make decisions about the positioning of the actual oil paintings to best effect. We are grateful to Gwen Adair for permission to use photographs of her oil paintings. She holds the copyright of both of the photographs used by this program. We wait until both the images have loaded before processing them in a similar way to those on the previous page. You need to remember to add each image that you use to Resources (in the left panel of the Smart Mobile Studio IDE) so that they will be copied to the res folder. We use the WinPhone8.css theme.

The Smart Pascal and XML code of the form (developed using Version 2.2 Beta 5 of Smart Mobile Studio) follow the demonstration. If you see no display at school, the security system might have blocked it. You can try instead this direct link to the program running on its own page. We have tested it to our satisfaction using the browsers Chrome, Opera and Firefox. Edge did not render the scene until we clicked on a list box.

Demo

An easy challenge is to change the colours of the floor, ceiling and both walls. Can you add another texture to a wall?

Smart Pascal Code of Form

unit Form1;
{ BOTH IMAGES COPYRIGHT GWEN ADAIR https://www.facebook.com/gwenadairpainter/ and
https://gwenadair.wixsite.com/painter}
interface

uses
  W3C.TypedArray, SmartCL.System, SmartCL.Graphics,
  SmartCL.Controls, SmartCL.Components, SmartCL.Forms, SmartCL.Fonts,
  SmartCL.Borders, SmartCL.Application, SmartCL.Controls.WebGL,
  Khronos.WebGl, GLS.Vectors, GLS.Base, W3C.Canvas2DContext, System.Colors;

type
  TForm1 = class(TW3form)
    procedure lbDistancesClick(Sender: TObject);
    procedure lbAnglesClick(Sender: TObject);
  private
    {$I 'Form1:intf'}
  protected
    FFilepaths: array[0..1] of string = ['res/Lee.jpg', 'res/RiverNithAtDumfries.jpg'];
    gl: JWebGLRenderingContext;
    rc: TGLRenderingContext;
    FCubeBuffer, FColorBuffer, FPlacementBuffer, FTexCoordBuffer: TGLArrayBuffer;
    FTexFragShader, FColFragShader: TGLFragmentShader;
    FTexVertexShader, FColVertShader: TGLVertexShader;
    FTexShaderProg, FColShaderProg: TGLShaderProgram;
    FVertexPosAttrib, FTexCoordAttrib, FColorAttrib, FPlacementAttrib, FNumTextures, FLastImageNum: integer;
    FImages: array of TW3Image;
    data: array of JImageData;
    textures: array of JWebGLTexture;
    procedure InitializeObject; override;
    procedure Resize; override;
    procedure SetupScene;
    procedure Render(YRot, DistFromCentre: Float);
    procedure ProcessImage(Sender : TObject);
    procedure Adjust;
  end;

implementation

procedure TForm1.Adjust;
begin
  render(lbAngles.SelectedIndex * 0.27 , (lbDistances.SelectedIndex + 4) / 2 );
end;

procedure TForm1.lbAnglesClick(Sender: TObject);
begin
  Adjust;
end;

procedure TForm1.lbDistancesClick(Sender: TObject);
begin
  Adjust;
end;

procedure TForm1.InitializeObject;
begin
  inherited;
  {$I 'Form1:impl'}
  FNumTextures := length(FFilepaths);
  FLastImageNum := FNumTextures - 1;
  for var i := 0 to FLastImageNum do
    begin
      FImages[i] := TW3Image.Create(nil);
      FImages[i].LoadFromURL(FFilepaths[i]);
      FImages[i].OnLoad := ProcessImage;
    end;
  gl := WebGLCanvas.Scene.Handle;
  rc := TGLRenderingContext.Create;
  rc.gl := gl;
  // Set up the list boxes for angles and distances.
  for var i := 0 to 6 do
  begin
    lbAngles.Add;
    lbAngles.Items[i].InnerText := inttostr(i * 15);
    lbDistances.Add;
    lbDistances.Items[i].InnerText := floattostr( (i + 4) / 2 );
  end;
  lbAngles.Styles.SelectedColor := clLightBlue;
  lbAngles.SelectedIndex := 3;
  lbAngles.EnableAnimation := false;
  lbDistances.Styles.SelectedColor := clLightGreen;
  lbDistances.SelectedIndex := 3;
  lbDistances.EnableAnimation := false;
end;

procedure TForm1.ProcessImage(Sender : TObject);
begin
  dec (FNumTextures);
  if FNumTextures = 0 then
    begin
      for  var i := 0 to FLastImageNum do
        if FImages[i].Handle and FImages[i].Ready then
          data[i] := JImageData(FImages[i].Handle)
        else
          ShowMessage('Error: image unready');
      SetupScene;
      Adjust;
    end;
end;

procedure TForm1.Resize;
begin
  inherited;
  WebGLCanvas.width := Min(Width, 500);
  WebGLCanvas.height := Min(Height, 500);
end;

procedure TForm1.SetupScene;
begin
  gl.clearColor(0.9, 0.9, 0.9, 1.0);  // Set to light grey, fully opaque
  gl.clearDepth(1.0);                 // Clear everything
  gl.enable(gl.DEPTH_TEST);           // Enable depth testing
  gl.depthFunc(gl.LEQUAL);            // Near things obscure far things
  // Set viewport to the bounds of the canvas.
  gl.ViewportSet(0, 0, WebGLCanvas.Width, WebGLCanvas.Height);
  // Create and populate buffer for texture coordinates.
  FTexCoordBuffer := TGLArrayBuffer.Create(rc);
  FTexCoordBuffer.SetData([
     // first texture
     0.0,  0.0, // top left
     1.0,  0.0, // top right
     0.0,  1.0, // bottom left
     1.0,  1.0, // bottom right
     // second texture as above
     0.0,  0.0,
     1.0,  0.0,
     0.0,  1.0,
     1.0,  1.0
  ], abuStatic);
  // Create and populate buffer for colouring faces of cube.
  FCubeBuffer := TGLArrayBuffer.Create(rc);
  FCubeBuffer.SetData([ // Vertices of back and right faces of cube
    // Axis Y increases up screen and Z increases out of front of screen
    // back face
    -1.0,  1.0, -1.0, // top left
     1.0,  1.0, -1.0, // top right
    -1.0, -1.0, -1.0, // bottom left
     1.0, -1.0, -1.0, // bottom right
    // right face
     1.0,  1.0, -1.0,
     1.0,  1.0,  1.0,
     1.0, -1.0, -1.0,
     1.0, -1.0,  1.0,
    // bottom face
    -1.0, -1.0, -1.0,
     1.0, -1.0, -1.0,
    -1.0, -1.0,  1.0,
     1.0, -1.0,  1.0,
    // top face
    -1.0, 1.0,  1.0,
     1.0, 1.0,  1.0,
    -1.0, 1.0, -1.0,
     1.0, 1.0, -1.0
   ], abuStatic);

   // Create and populate buffer for colours of faces of cube.
  FColorBuffer := TGLArrayBuffer.Create(rc);
  FColorBuffer.SetData([ // Colours of vertices of faces of cube
    // back face
    0.891, 0.852, 0.758,
    0.891, 0.852, 0.758,
    0.891, 0.852, 0.758,
    0.891, 0.852, 0.758,
    // right face
    0.953, 0.93, 0.879,
    0.953, 0.93, 0.879,
    0.953, 0.93, 0.879,
    0.953, 0.93, 0.879,
    // bottom  grey
    0.7, 0.7, 0.7,
    0.7, 0.7, 0.7,
    0.7, 0.7, 0.7,
    0.7, 0.7, 0.7,
    // top white
    1.0, 1.0, 1.0,
    1.0, 1.0, 1.0,
    1.0, 1.0, 1.0,
    1.0, 1.0, 1.0
  ], abuStatic);

  // Create and populate buffer for positioning textures on faces of the cube.
  FPlacementBuffer := TGLArrayBuffer.Create(rc);
  FPlacementBuffer.SetData([
    // Axis y increases up screen and z increases out of front of screen
    // back face
    0.0,  0.8, -0.95, // half way along face, near top
    0.8,  0.8, -0.95, // near top right corner of face
    0.0,  0.0, -0.95, // near bottom left, half way down face
    0.8,  0.0, -0.95, // near right edge, half way down face
    // right face
    0.95,  0.8, -0.8,
    0.95,  0.8,  0.0,
    0.95,  0.0, -0.8,
    0.95,  0.0,  0.0
  ], abuStatic);

  // create vertex shader for colouring faces
  FColVertShader := TGLVertexShader.Create(rc);
  FColVertShader.Compile(#"
    attribute vec3 a_position;
    attribute vec3 aVertexColor;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    varying vec4 vColor;

    void main() {
      vColor = vec4(aVertexColor, 1.0);
      gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(a_position, 1.0);
    }");

  // Create fragment shader for colouring faces.
  FColFragShader := TGLFragmentShader.Create(rc);
  FColFragShader.Compile(#"
    precision mediump float;
    varying vec4 vColor;

    void main() {
      gl_FragColor = vColor;
    }");

  // Create the shader program for colour and link shaders.
  FColShaderProg := TGLShaderProgram.Create(rc);
  FColShaderProg.Link(FColVertShader, FColFragShader);
  // Create the vertex shader for textures.
  FTexVertexShader := TGLVertexShader.Create(rc);
  FTexVertexShader.Compile(#"
    attribute vec3 aVertexPosition;
    attribute vec2 a_texCoord;
    varying vec2 v_texCoord;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    void main(void) {
      gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
      v_texCoord = a_texCoord;
    }");

  // Create the fragment shader for textures.
  FTexFragShader := TGLFragmentShader.Create(rc);
  FTexFragShader.Compile(#"
    precision mediump float;
    uniform sampler2D u_image;
    varying vec2 v_texCoord;

    void main(void) {
      gl_FragColor = texture2D(u_image, v_texCoord);
    }");

  // Create the shader program and link the shaders for textures.
  FTexShaderProg := TGLShaderProgram.Create(rc);
  FTexShaderProg.Link(FTexVertexShader, FTexFragShader);
  FTexCoordAttrib := FTexShaderProg.AttribLocation("a_texCoord");
  for var i := 0 to FLastImageNum do
    begin
      textures[i] := gl.createTexture;
      gl.activeTexture(gl.TEXTURE0 + i);
      gl.bindTexture(gl.TEXTURE_2D, textures[i]);
      // Set the parameters so we can render any size image.
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
      // Upload the image into the texture.
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data[i]);
    end;
end;

procedure TForm1.Render(YRot, DistFromCentre: Float);
var
  ProjectionMatrix, ModelViewMatrix : Matrix4;
  ModelViewStack : array of Matrix4;

  procedure RenderCube(z : Float);
  begin
    ModelViewStack.Push(ModelViewMatrix); // Saves the matrix
    ModelViewMatrix := ModelViewMatrix.Translate([0, 0, z]);
    ModelViewMatrix := ModelViewMatrix.RotateY(YRot);
    // Colour four faces.
    FColShaderProg.Use;
    FColShaderProg.SetUniform('uProjectionMatrix', ProjectionMatrix);
    FColShaderProg.SetUniform('uModelViewMatrix', ModelViewMatrix);
    FVertexPosAttrib := FColShaderProg.AttribLocation("a_position");
    FColorAttrib :=  FColShaderProg.AttribLocation("aVertexColor");
    // Enable arrays and set pointers to them.
    gl.enableVertexAttribArray(FVertexPosAttrib);
    gl.enableVertexAttribArray(FColorAttrib);
    FCubeBuffer.VertexAttribPointer(FVertexPosAttrib, 3, False, 0, 0);
    FColorBuffer.VertexAttribPointer(FColorAttrib, 3, False, 0, 0);
    // Draw
    for var i := 0 to 12 step 4 do
      gl.drawArrays(gl.TRIANGLE_STRIP, i, 4);
    // Display the textures.
    FTexShaderProg.Use;
    FTexShaderProg.SetUniform('uProjectionMatrix', ProjectionMatrix);
    FTexShaderProg.SetUniform('uModelViewMatrix', ModelViewMatrix);
    FPlacementAttrib := FTexShaderProg.AttribLocation("aVertexPosition");
    // Enable arrays and set pointers to them.
    gl.enableVertexAttribArray(FPlacementAttrib);
    gl.enableVertexAttribArray(FTexCoordAttrib);
    FPlacementBuffer.VertexAttribPointer(FPlacementAttrib, 3, False, 0, 0);
    FTexCoordBuffer.VertexAttribPointer(FTexCoordAttrib, 2, False, 0, 0);
    // Bind each texture then display it.
    gl.activeTexture(gl.TEXTURE0);
    for var i := 0 to FLastImageNum do
      begin
        gl.bindTexture(gl.TEXTURE_2D, textures[i]);
        gl.drawArrays(gl.TRIANGLE_STRIP, i * 4, 4);
      end;
    ModelViewMatrix := ModelViewStack.Pop; // Restores the matrix
  end;

begin
  // Clear the background.
  gl.clear(gl.COLOR_BUFFER_BIT);
  // Camera field of view of 45 shows the cube just fitting into the canvas.
  ProjectionMatrix := Matrix4.CreatePerspective(40, WebGLCanvas.Width / WebGLCanvas.Height,
                                                0.1, 100);
  ModelViewMatrix := Matrix4.Identity;
  RenderCube(-DistFromCentre);
end;

initialization
  Forms.RegisterForm({$I %FILE%}, TForm1);
end.
    

XML Code of Form

<SMART>
  <Form version="2" subversion="2">
    <Created>2019-09-17T10:03:34.254</Created>
    <Modified>2019-10-03T11:58:10.832</Modified>
    <object type="TW3Form">
      <Caption>W3Form</Caption>
      <Name>Form1</Name>
      <object type="TW3WebGL">
        <Width>504</Width>
        <Height>496</Height>
        <Name>WebGLCanvas</Name>
      </object>
      <object type="TW3ListBox">
        <Width>48</Width>
        <Top>80</Top>
        <Left>512</Left>
        <Height>240</Height>
        <Name>lbAngles</Name>
        <OnClick>lbAnglesClick</OnClick>
      </object>
      <object type="TW3ListBox">
        <Width>57</Width>
        <Top>80</Top>
        <Left>587</Left>
        <Height>240</Height>
        <Name>lbDistances</Name>
        <OnClick>lbDistancesClick</OnClick>
      </object>
      <object type="TW3Label">
        <Caption>Viewing&lt;br&gt;angle&lt;br&gt;(degrees)</Caption>
        <Width>96</Width>
        <Top>8</Top>
        <Left>512</Left>
        <Height>64</Height>
        <Name>lblAngle</Name>
      </object>
      <object type="TW3Label">
        <Caption>Distance from&lt;br&gt;back wall&lt;br&gt;(metres)</Caption>
        <Width>96</Width>
        <Top>8</Top>
        <Left>584</Left>
        <Height>62</Height>
        <Name>lblDistance</Name>
      </object>
    </object>
  </Form>
</SMART>

Using Version 3.0 of Smart Mobile Studio

Smart Mobile Studio 3.0, when attempting to compile the project (.sproj) file containing the code above, reported a syntax error: 'There is no accessible member with name "InnerText"'. We changed the list box code to the following to enable it to compile.
  // Set up the list boxes for angles and distances.
  for var i := 0 to 6 do
  begin
    lbAngles.AddItem(inttostr(i * 15));
    lbDistances.AddItem(floattostr( (i + 4) / 2 ));
  end;
  lbAngles.SelectedIndex := 3;
  lbDistances.SelectedIndex := 3;    
The second demo was produced from an earlier unfinished version of the project output by the command line compiler of Smart Mobile Studio 3.0. It uses edit boxes as spin edit controls and a slider to perform the functions of the list boxes in the first demo. Unlike the output from earlier compilers, the HTML file does not render correctly when run directly from a folder on a local PC. We include this as proof that the same HTML file on a website will display images (that are stored on the same site). We invite you to make improvements such as these three.
  1. Put the slider directly below the edit box for the angle.
  2. Make the slider display update if the angle is changed using the edit box.
  3. Add text to explain what the controls do (as in the top demo).
Demo
Most of the code is the same but these code fragments replace the list box code above. The different procedures are
procedure TForm1.Adjust;
begin
  Render(strToInt(W3EditBox1.Text) * 0.1, strToInt(W3EditBox2.Text) * 0.5);
end;

procedure TForm1.W3EditBox1Click(Sender: TObject);
begin
  Adjust;
end;

procedure TForm1.W3EditBox2Click(Sender: TObject);
begin
  Adjust;
end;

procedure TForm1.W3Slider1Change(Sender: TObject);
begin
  W3EditBox1.Text := IntToStr(Round(W3Slider1.Value));
  Adjust;
end;.
These lines of code are at the end of the InitializeObject procedure.
  W3EditBox1.InputType := itNumber;
  W3EditBox1.MinValue := 0;
  W3EditBox1.MaxValue := 16;
  W3EditBox1.Text := '8';
  W3EditBox2.InputType := itNumber;
  W3EditBox2.MaxValue := 8;
  W3EditBox2.MinValue := 4;
  W3EditBox2.Text := '6';    

See Using an EditBox as a SpinEdit Control for code variations for earlier versions of Smart Mobile Studio.

Programming - a skill for life!

Using textures and texture coordinates to render four different images on the faces of a cube