2020年12月1日 星期二

WebGL的繪製三次方赫密特曲線( Cubic hermite curve )的切線

 WebGL的繪製三次方赫密特曲線( Cubic hermite curve )的切線

前言

  在先前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線 裡繪製了貝茲曲線的切線,這次來繪製三次方赫密特曲線( Cubic hermite curve )的切線,在此把學習的過程做個紀錄。


內容

  在先前的 WebGL的繪製三次方赫密特曲線( Cubic hermite curve ) 裡已經有一個繪製曲線的範例,這次的範例會從該專案改出來。在之前不確定"微分"後就可以得到切線,這次算是得到一個驗證,所以如果將赫密特曲線拿去為分會得到以下

切線公式

接著將這個公式套用到先前的範例,程式碼如下

HTML 的部分

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCubicHermiteCurve"type="button" value="DrawCubicHermiteCurve"/>
<br>
<input id="btnDrawCubicBezierCurve"type="button" value="DrawCubicBezierCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
<br>
<input type="checkbox" id="isShowTangent" checked><label>Show tangent </label>
<input type="range" min="0" max="1" value="0.5" step="0.001" class="slider" id="sliderTangent">
<label id="labelTangentValue">0.5</label>
</body>
</html>



Javascript 的部分

let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
let curveType = 0;
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateLineListArrayData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return tagAr;
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT * invT;
    let part1Value = 3 * t * invT * invT;
    let part2Value = 3 * t * t * invT;
    let part3Value = t * t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function calCubicHermiteCurvePosition(p0,s0,p1,s1,t){
  let invT = 1.0-t;
  let part0Value = (1 + ( 2 * t ) ) * ( invT * invT );
  let part1Value = t * ( ( invT * invT ) );
  let part2Value = ( t * t ) * ( 3 - ( 2 * t) );
  let part3Value = ( t * t ) * ( t - 1.0 );
  let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
  let part1 = [part1Value*s0[0], part1Value*s0[1], part1Value*s0[2] ];
  let part2 = [part2Value*p1[0], part2Value*p1[1], part2Value*p1[2] ];
  let part3 = [part3Value*s1[0], part3Value*s1[1], part3Value*s1[2] ];
  let position = [
    part0[0] + part1[0] + part2[0] + part3[0],
    part0[1] + part1[1] + part2[1] + part3[1],
    part0[2] + part1[2] + part2[2] + part3[2]
  ];
  return position;
}
function generateCubicHermiteCurve(p0,s0,p1,s1,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let pos = calCubicHermiteCurvePosition(p0,s0,p1,s1,t);
    tagAr.push( pos[0] );
    tagAr.push( pos[1] );
    tagAr.push( pos[2] );
  }
  return tagAr;
}
function calCubicHermiteCurveTangent(p0,s0,p1,s1,t){
  let part0Value = ( 6 * t * t ) - ( 6 * t);
  let part1Value = ( 3 * t * t ) - ( 4 * t) + 1;
  let part2Value = ( 6 * t) - ( 6 * t * t );
  let part3Value = ( 3 * t * t ) - ( 2 * t );
  let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
  let part1 = [part1Value*s0[0], part1Value*s0[1], part1Value*s0[2] ];
  let part2 = [part2Value*p1[0], part2Value*p1[1], part2Value*p1[2] ];
  let part3 = [part3Value*s1[0], part3Value*s1[1], part3Value*s1[2] ];
  let tangent = [
    part0[0] + part1[0] + part2[0] + part3[0],
    part0[1] + part1[1] + part2[1] + part3[1],
    part0[2] + part1[2] + part2[2] + part3[2]
  ];
  return tangent;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
let tagTangentValue=document.getElementById("sliderTangent").value;
function UpdateCubicBezierCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
function UpdateCubicHermiteCurveData(){
  let data=generateCubicHermiteCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[3],
    ctrlPointList[2],
    tagLerpValue);
  //
  let dataArray=generateLineListArrayData(data);
  let isShowTangent = document.getElementById("isShowTangent").checked;
  if(isShowTangent){    
    let tagT = tagTangentValue;
    let pos = calCubicHermiteCurvePosition(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[3],
      ctrlPointList[2],
      tagT
    );
    let tangent = calCubicHermiteCurveTangent(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[3],
      ctrlPointList[2],
      tagT
    );
    dataArray.push(pos[0]);
    dataArray.push(pos[1]);
    dataArray.push(pos[2]);
    dataArray.push(pos[0]+tangent[0]);
    dataArray.push(pos[1]+tangent[1]);
    dataArray.push(pos[2]+tangent[2]);
  }

  
  let tagDataArray = new Float32Array( dataArray );
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,tagDataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCubicBezierCurve").onclick=function(evt){
  UpdateCubicBezierCurveData();
  curveType = 1;
}
document.getElementById("btnDrawCubicHermiteCurve").onclick=function(evt){
  UpdateCubicHermiteCurveData();
  curveType = 2;
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  //UpdateCurveData();
  switch( curveType ){
    case 1:
      UpdateCubicBezierCurveData();
      break;
    case 2:
      UpdateCubicHermiteCurveData();
      break;
      
  }
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
document.getElementById("isShowTangent").onclick=function(){
  UpdateCubicHermiteCurveData();
}
document.getElementById("sliderTangent").oninput=function(evt){
  tagTangentValue=this.value;
  UpdateCubicHermiteCurveData();
  document.getElementById("labelTangentValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}


執行結果如下

範例的執行結果


這次的改法跟 WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線 的改法是一樣的,所以做不多做說明。


  這次算是對微分後得到切線方程式做一個驗證,以後碰到的曲線方程式都可以利用微分得到切線,算是這次的收穫。


參考資料

[ wiki ] Cubic Hermite spline


相關文章與資料

WebGL的繪製三次方赫密特曲線( Cubic hermite curve )

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線


沒有留言:

張貼留言