WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線
前言
在先前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 繪製了曲線,但最近需要得到 tangent (切線) ,所以就研究了一下如何計算切線,在此把學習的過程做個紀錄。
內容
要計算切線的話我在 [ stackoverflow.com ] Find the tangent of a point on a cubic bezier curve 裡找到了公式如下
這個公式也可以在 [ en.wikipedia.org ] Bézier curve 裡面找到,限定在英文的 Wiki 才找的到請注意。
接著把公式套到 WebGL的繪製三次方貝茲曲線( Cubic bezier 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="btnDrawCurve"type="button" value="DrawCurve"/> <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);
//
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 calCubicBezierCurvePosition(p0,p1,p2,p3,t){
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] ];
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 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 pos = calCubicBezierCurvePosition(p0,p1,p2,p3,t);
tagAr.push( pos[0] );
tagAr.push( pos[1] );
tagAr.push( pos[2] );
}
return tagAr;
}
function calCubicBezierCurveTangent(p0,p1,p2,p3,t){
let del10 = [ p1[0] - p0[0] , p1[1] - p0[1] , p1[2] - p0[2] ];
let del21 = [ p2[0] - p1[0] , p2[1] - p1[1] , p2[2] - p1[2] ];
let del32 = [ p3[0] - p2[0] , p3[1] - p2[1] , p3[2] - p2[2] ];
let invT = 1.0-t;
let part0Value = 3 * invT * invT ;
let part1Value = 6 * t * invT;
let part2Value = 3 * t * t;
let tangent = [
part0Value * del10[0] + part1Value * del21[0] + part2Value * del32[0],
part0Value * del10[1] + part1Value * del21[1] + part2Value * del32[1],
part0Value * del10[2] + part1Value * del21[2] + part2Value * del32[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 UpdateCurveData(){
let data=generateCubicBezierCurve(
ctrlPointList[0],
ctrlPointList[1],
ctrlPointList[2],
ctrlPointList[3],
tagLerpValue);
//
let dataArray=generateLineListArrayData(data);
let isShowTangent = document.getElementById("isShowTangent").checked;
if(isShowTangent){
let tagT = tagTangentValue;
let pos = calCubicBezierCurvePosition(
ctrlPointList[0],
ctrlPointList[1],
ctrlPointList[2],
ctrlPointList[3],
tagT
);
let tangent = calCubicBezierCurveTangent(
ctrlPointList[0],
ctrlPointList[1],
ctrlPointList[2],
ctrlPointList[3],
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("btnDrawCurve").onclick=function(evt){
UpdateCurveData();
}
document.getElementById("sliderLerp").oninput=function(evt){
tagLerpValue=this.value;
UpdateCurveData();
document.getElementById("labelLerpValue").innerHTML = this.value;
}
document.getElementById("isShowTangent").onclick=function(){
UpdateCurveData();
}
document.getElementById("sliderTangent").oninput=function(evt){
tagTangentValue=this.value;
UpdateCurveData();
document.getElementById("labelTangentValue").innerHTML = this.value;
}
window.onload = function(){
window.requestAnimationFrame(myRender);
}
執行結果如下
![]() |
| 範例的執行結果 |
這次的程式碼改得有點亂,跟之前的範例不一樣的部分會用紅字來表示,這次提供了 generateLineListArrayData() ,跟之前 generateLineListData() 的差異是回傳的資料型態不一樣,
generateLineListArrayData() 回傳的型態就是單純的 Array ,這是方便這次需要多畫切線要插入資料。 calCubicBezierCurvePosition() 是提供利用百分比來取得曲線的位置, generateCubicBezierCurve() 跟上次不太一樣,取得位置的部分會用 calCubicBezierCurvePosition() 來替代,計算的結果是一樣的,單純的程式碼優化。 calCubicBezierCurveTangent() 這次的主角,套用公式就可以取得該點的切線。接著看到 UpdateCurveData() ,這次由於需要多畫切線,所以用 generateLineListArrayData() 取得資料後再來插入切線資料,最後才轉成 Float32Array ,切線算出來的是變化量,所以要加上該點後才能形成切線的資料。
切線的公式似乎是將原本的曲線公式作微分的動作,不過由於我數學不好所以不太確定,以後有機會再來研究看看。
參考資料
[ en.wikipedia.org ] Bézier curve
[ stackoverflow.com ] Find the tangent of a point on a cubic bezier curve

沒有留言:
張貼留言