作者:AlloyTeam
網址:http://www.alloyteam.com/2015/08/fu-li-ru-yao-dong-hua-chu-tan/
咳,以探索技術的精神進行一些猥瑣的實現,先說明,如果你只想看最後乳搖的結果那就請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點,完成液化
最終給個結果圖
因為只時間緣故(LPL決賽呢)只設定了左胸的搖動觸發,觸發程式碼如下
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就不放出來了……一些幅度,方向都很簡單,而且是寫死的,如果你有興趣可以更多的去最佳化和新增功能~