咳,以探索技術的精神進行一些猥瑣的實現,先說明,如果你只想看最后乳搖的結果那就請ctrl+F4吧,因為網上有那些乳搖的APP,制作出來絕對比我這個初探的方法好,我這個只是介紹我實現乳搖的過程思路與方法。
關於乳搖如何實現,我第一個想法是使用metaball,因為是兩個球嘛,然而發現根本就不行,Fail。最終使用的是液化算法去實現。
好了,下面是對液化算法的介紹。
如果你從來不使用ps,暫時想不起來液化是什么不要緊,請看下圖
這個是采用液化使一只靜態的小狗有了動的感覺
總而言之,液化是使一張圖片的部分進行平滑的有規律的變化,這個變化有扭曲的平移之感
是如何實現的呢,來看算法,先給一張圖
局部變化以一個圈為變化環境,C點是圓心,r是半徑,當C移動到M這個點時,U移動到X。
通過以上這句話我們得出了這樣的結論:
液化的變化只在半徑為r的圓內發生,距離圓心越近,變化越明顯。這是不是和乳搖這一現象特別的吻合?
下面是算法公式
公式終於實現的是你知道X點的坐標,可以推算出U點的坐標,反之知道U點也可以推算出X的坐標
怎么推算出來的這個我也不知道,需要結合圓的范圍,進行插值處理,但是具體如何得到這個結論,感興趣的同學可以閱讀
Andreas Gustafsson 的 Interactive Image Warping一文,這個公式就是從這而來
好了,接下來該碼程序了
乳搖首先你得有圖
var sImg = new Image(); sImg.src = './dd.png'; var leftImage = new Image(); leftImage.src = './dd_left.png'; var rightImage = new Image(); rightImage.src = './dd_right.png'; var timer = null; sImg.onload = function() { oGC.drawImage(sImg, 0, 0); };
這里上來就有三張圖,其中一張是完整的,還有兩張分別是美女左右對半分開的【其實就是左胸和右胸】,因為需要這兩張圖進行乳搖【液化】后的還原。當然你也可以只用一張圖,先取出還原的像素存起來也是可以的,我在這里偷懶了
function liquify(imgData, cx, cy, mx, my, r) { var imgDataBuff = copyImageDataBuff(imgData); eachCircleDot(imgData, cx, cy, r, function(posi) { var tx = posi.x, ty = posi.y; var u = transFormula(cx, cy, mx, my, tx, ty, r); moveDot(imgData, imgDataBuff, posi, u); function transFormula(cx, cy, mx, my, tx, ty, r) {var relativity = sqr(r) - distanceSqr(tx, ty, cx, cy); var distanceMovedSqr = distanceSqr(mx, my, cx, cy); var rate = sqr(relativity / (relativity + distanceMovedSqr)); var ux = (tx - rate*(mx-cx)), uy = (ty - rate*(my-cy)); return { x: ux, y: uy }; } }); return imgData; }
上面是液化算法的函數,結合上面的圖來看,參數分別是圖片像素data,圓心C的x軸和y軸,M點的x軸和y軸,圓的半徑r
copyImageDataBuff是將將要液化部分的像素copy一份,代碼如下
function copyImageDataBuff(imgData) { var data = imgData.data, imgDataBuff = []; for(var i in data) { imgDataBuff[i] = data[i]; } return imgDataBuff; }
eachCircleDot是將每個圓內的像素取出來進行處理
function eachCircleDot(imageData, ox, oy, r, callback) { var imgWidth = imageData.width, imgHeight = imageData.height, data = imageData.data, left = ox - r, right = ox + r, top = oy - r, bottom = oy + r; for(var x = left; x < right; x++) { for(var y = top; y < bottom; y++) { if(distanceSqr(x,y,ox,oy) <= sqr(r)) { callback({ x: x, y: y }); } } } }
distanceSqr和sqr是求圓心距離和平方的函數,很簡單
function distanceSqr(x1, y1, x2, y2) { return sqr(x1 - x2) + sqr(y1 - y2); } function sqr(x) { return x * x; }
transFormula這個方法就是液化公式的使用,傳入的是c點的x,y值、m點的x,y;t點就是上面圖中的x點,return出來u點的x,y值之后傳入moveDot,這個就是液化最終的表現函數
function moveDot(imgData, dataBuff, posi, u) { var imgWidth = imgData.width, imgHeight = imgData.height, data = imgData.data; u.x = Math.floor(u.x); u.y = Math.floor(u.y); data[(posi.y * imgWidth + posi.x) * 4] = dataBuff[(u.y * imgWidth + u.x) * 4]; data[(posi.y * imgWidth + posi.x) * 4 + 1] = dataBuff[(u.y * imgWidth + u.x) * 4 + 1]; data[(posi.y * imgWidth + posi.x) * 4 + 2] = dataBuff[(u.y * imgWidth + u.x) * 4 + 2]; data[(posi.y * imgWidth + posi.x) * 4 + 3] = dataBuff[(u.y * imgWidth + u.x) * 4 + 3]; }
將公式算出的u點rgba信息換給之前的t點,也就是圖中的x點,完成液化
最終給一個成果圖
因為時間緣故只設置了左胸的搖動觸發,觸發代碼如下
var sX = 5; var sY = 5; var iX = -200; var x = -10;
var iY = ev.clientY - oC.offsetTop; if(iY > 296) { iY = 200; y = 10; } else { iY = -200; y = -10; } timer = setInterval(function() { oGC.drawImage(leftImage, 0, 0); // 只做了左半邊的效果 var d = oGC.getImageData(23, 140, 140, 200); var c = liquify(d, 60, 170, sX + 65, sY + 170, 58); oGC.putImageData(c, 23, 140); sX = sX + x; sY = sY + y; if(Math.abs(sX) > Math.abs(iX) || Math.abs(sY) > Math.abs(iY)) { clearInterval(timer); } }, 30);
這里面的數值都是自己測出來的,sX和sY是搖動的頻率,getImageData的xywh四個值也是試出來的的,意味着你想要胸變化的范圍大小,注意:
這個范圍必須要比液化公式中的圓大
iY和y是根據點擊在胸上方還是胸下方來確定搖動的方向
liquify傳入的參數已經介紹過了
——————————————————————————————————————————————————————————————
這次的乳搖還是很初步的,只是優化了速度,最早還有一個版本非常的卡,demo就不放出來了……一些幅度,方向都很簡單,而且是寫死的,如果你有興趣可以更多的去優化和添加功能~