スクリプト

マスクでひび割れ分割するスクリプト「crackMask.jsx」

レイヤーをクモの巣のようにひび割れさせたい場合、シャターエフェクトでも手軽に実装できますが、欠片を個別に制御したいこともあるでしょう。1つの欠片を1つのレイヤーに分割します。

パズルのようにマスクで分割された全てのレイヤーが初期位置で元の形を維持します。

[概要]

「割れたガラスっぽいマスク」で複数のレイヤーに分割するスクリプト。なので全パーツが手動で制御できる。

[使い方]

  1. スクリプトを実行しUIを表示させる
  2. レイヤーを1つ選択し[1. 中心指定 追加]ボタンでポイント制御エフェクトを追加する
  3. 追加された「割れの中心」エフェクトでひび割れの基点を指定する
  4. [3. Crack!!]ボタンをクリックする

[オプション]

追加されるコントローラー用ヌルレイヤー「crackMask_Control」を使って、

  • ヌルレイヤー自体を移動で全ピースが移動
  • [全ピース中心からオフセット]…全ピースを中心から外側へ向かう直線上移動
  • [全ピース回転 X][全ピース回転 Y][全ピース回転 Z]…全ピース回転
  • [全ピースランダム回転]…全ピースをバラバラに回転

できるようにしています。

また、[3. Crack!!]ボタンをクリックする前に設定内の数値で色々調節できます。

パラメータ意味推奨範囲(目安)調整のコツ
角度分割中心から外へ伸びる「放射線」の本数。整数で指定。8 〜 24 (少なすぎると割れが粗く、多すぎると処理が重くなる)値を増やすと放射線が細かくなり、より多くの破片が生まれます。減らすと大きな ギザギザのピースになります。
角度ランダム変数各放射線の角度に加えるランダム量(ラジアン単位)。0 以上。0.00 〜 0.20 (0.05〜0.12 が自然なばらつき)値を大きくすると放射線が不規則になり、クラックの方向がよりランダムに。0 に近づけると完全に規則的な放射線になる。
内→外分割放射線に沿って作られる「同心円」の分割数(中心から外へのリング数)。整数で指定。2 〜 6値を増やすとリングが増えて、放射線と同心円を組み合わせた格子状の割れになります。減らすと放射線主体の割れになる。
内→外角度ランダム変数 各同心円ポイントの角度に加えるランダム量(ラジアン)。0 以上。0.00 〜 0.15値が小さいほど規則的に、大きくすると同心円がゆがみ、放射線との交点が不規則になる。
端の調整分割 レイヤーの外枠(矩形)に到達する放射線の端をさらに細分する数。整数で指定。1 〜 4値を増やすとレイヤー端でのクラックが滑らかになり、端がぎざぎざになりにくい。1 だと端はそのままの直線になる。
同心円崩れ各同心円の半径に加えるランダム量(0〜1 の相対値)。0 以上。0.10 〜 0.30値を大きくすると同心円が不均等に広がり収縮し、「波打った」クラックになる。小さいほど真円に近い。

注意:数値を極端に大きくすると計算量が増え、プレビューやレンダリングが遅くなる。まずは低めの値から少しずつ試してみてください。

[解説]

JavaScript
var scName="crackMask";
var scVersion="1.0";

// ==================================================
// 定数
// ==================================================
var centerNull=null;

var fxName_dup1="外側へオフセット";
var fxName_src1="割れの中心";
var fxName_src2="全ピース中心からオフセット";
var fxName_src3="全ピース回転 X";
var fxName_src4="全ピース回転 Y";
var fxName_src5="全ピース回転 Z";
var fxName_src6="全ピースランダム回転";


// ==================================================
// GUI準備
// ==================================================

function buildUI(thisObj){
		var pnl=(thisObj instanceof Panel)
				? thisObj
				: new Window("palette",scName,undefined,{resizeable: true});

		if(!pnl)return pnl;

		pnl.orientation="column";
		pnl.alignChildren=["fill", "top"];
		pnl.spacing=8;
		pnl.margins=12;

		var btn1=pnl.add("group");
			btn1.orientation="row";
			btn1.alignChildren=["fill", "center"];
			btn1.spacing=8;

		var addPointBtn=btn1.add("button",undefined,"1. 中心指定 追加");
			addPointBtn.onClick=function(){
				try{
						f_addCenterPoint();
				}catch(e){
						alert( e.toString() );
				}
		};

		var setArea=pnl.add("panel",undefined,"2. 設定");
			setArea.orientation="column";
			setArea.alignChildren=["fill", "top"];
			setArea.margins=10;
			setArea.spacing=2;

		var input_angleSplit	=f_setInput(setArea,"角度分割", "12");
		var input_angleJitter	=f_setInput(setArea," ┗角度ランダム変数", "0.08");
		var input_radialSplit	=f_setInput(setArea,"内→外分割", "3");
		var input_radialJitter=f_setInput(setArea," ┗内→外角度ランダム変数", "0.10");
		var input_edgeSplit		=f_setInput(setArea,"端の調整分割", "2");
		var input_rayJitter		=f_setInput(setArea,"同心円崩れ", "0.18");

		var chk_lockMask=setArea.add("checkbox", undefined, "分割後のマスクをロック");
			chk_lockMask.value=true;

		var btn2=pnl.add("group");
			btn2.orientation="row";
			btn2.alignChildren=["fill", "center"];

		var runBtn=btn2.add("button", undefined, "3. Crack!!");
			runBtn.onClick=function(){
				try{
						var angleSplit	=parseInt(input_angleSplit.text, 10);
						var radialSplit =parseInt(input_radialSplit.text, 10);
						var edgeSplit		=parseInt(input_edgeSplit.text, 10);
						var angleJitter =parseFloat(input_angleJitter.text);
						var rayJitter		=parseFloat(input_rayJitter.text);
						var radialJitter=parseFloat(input_radialJitter.text);

						if (isNaN(angleSplit) || angleSplit < 3) throw new Error("角度分割は3以上にしてください。");
						if (isNaN(angleJitter) || angleJitter < 0) throw new Error("┗角度ランダム変数は0以上にしてください。");
						if (isNaN(radialSplit) || radialSplit < 1) throw new Error("内→外分割は1以上にしてください。");
						if (isNaN(rayJitter) || rayJitter < 0) throw new Error("┗内→外角度ランダム変数は0以上にしてください。");
						if (isNaN(edgeSplit) || edgeSplit < 1) throw new Error("端の調整分割は1以上にしてください。");
						if (isNaN(radialJitter) || radialJitter < 0) throw new Error("同心円崩れは0以上にしてください。");

						f_crackMask(
								angleSplit,
								radialSplit,
								edgeSplit,
								angleJitter,
								radialJitter,
								rayJitter,
								chk_lockMask.value
						);

				}catch(e){
						alert( e.toString() );
				}
		};

		pnl.layout.layout(true);
		pnl.layout.resize();
		pnl.onResizing=pnl.onResize=function(){ this.layout.resize(); };

		return pnl;
}

function f_setInput(parent,label,val){
		var gr_label=parent.add("group");
			gr_label.orientation="row";
			gr_label.alignChildren=["left", "center"];
			gr_label.spacing=8;

		var tx_label=gr_label.add("statictext", undefined, label);
			tx_label.preferredSize=[160, -1];

		var gr_edit=gr_label.add("group");
			gr_edit.orientation="row";
			gr_edit.alignChildren=["right", "center"];
			gr_edit.spacing=0;

		var tx_edit=gr_edit.add("edittext", undefined, val);
			tx_edit.characters=5;
			tx_edit.preferredSize=[60, -1];

		return tx_edit;
}


// ==================================================
// 基本ユーティリティ
// ==================================================

function f_rand(min,max){
		return min+Math.random()*(max-min);
}

function f_clamp(v,min,max){
		return Math.max( min,Math.min(max,v) );
}

// 「2点の間を t パーセントで区切って、途中の座標を決める」ために使う式
// 境界線上に点を等分割するのに使う
function f_lerp(a,b,t){
		return a+(b-a)*t;
}

// f_drawMask内部で、連続した頂点のうち、「ほとんど同じ座標の点」を削除
function f_organize(p){
		return Math.round(p[0]*1000)+"_"+Math.round(p[1]*1000);
}

// f_organizeと合わせて、閉じたマスク用に不要な、最初と最後が同じ点を除外
function f_deduplicate(verts){
		var out=[];
		var prev=null;
		for (var i=0;i<verts.length;i++){
				var p=verts[i];
				if( !prev || f_organize(prev) !== f_organize(p) ){
						out.push(p);
						prev=p;
				}
		}
		if( out.length>1 && f_organize(out[0]) === f_organize(out[out.length-1]) ){
				out.pop();
		}
		return out;
}

// 多角形の面積を求める有名な公式とのこと
function f_polygonArea(verts){
		var a=0;
		for(var i=0;i<verts.length;i++){
				var p1=verts[i];
				var p2=verts[(i+1)%verts.length];
				a+=p1[0]*p2[1]-p2[0]*p1[1];
		}
		return a*0.5;
}

// アンカーポイント用に多角形の重心を求める
function f_maskCentroid(verts){
		if(verts.length<3) return [0,0];

		var area=0;
		var cx=0;
		var cy=0;

		for (var i=0;i<verts.length;i++){
				var p1=verts[i];
				var p2=verts[(i+1)%verts.length];
				var cross=p1[0]*p2[1]-p2[0]*p1[1];
				area+=cross;
				cx+=(p1[0]+p2[0])*cross;
				cy+=(p1[1]+p2[1])*cross;
		}

		area*=0.5;

		if(Math.abs(area)<1e-10){
				return [
						(verts[0][0]+verts[verts.length-1][0])/2,
						(verts[0][1]+verts[verts.length-1][1])/2
				];
		}

		cx/=6*area;
		cy/=6*area;

		return [cx,cy];
}

function f_reverseVerts(arr){
		var out=[];
		for(var i=arr.length-1;i>=0;i--)out.push(arr[i]);
		return out;
}
// ==================================================
// Ae 操作系
// ==================================================

function f_getMaskGroup(layer){
		if(!layer) return null;
		return layer.property("ADBE Mask Parade");
}

function f_getEffectsGroup(layer){
		if(!layer) return null;
		return layer.property("ADBE Effect Parade");
}

function f_getLayerRect(layer,time){
		var sr=null;

		try{
				sr=layer.sourceRectAtTime(time,false);
		}catch(e){}

		if(sr && sr.width>0 && sr.height>0){
				return{
						left: sr.left,
						top: sr.top,
						width: sr.width,
						height: sr.height
				};
		}

		if(layer.width && layer.height){
				return {
						left: -layer.width/2,
						top: -layer.height/2,
						width: layer.width,
						height: layer.height
				};
		}

		return null;
}

function f_findFxName(layer,name,matchName){
		var effects=f_getEffectsGroup(layer);
		if (!effects) return null;

		for(var i=1;i<=effects.numProperties;i++){
				var eff=effects.property(i);
				if(!eff) continue;
				if(name && eff.name !== name) continue;
				if(matchName && eff.matchName !== matchName) continue;
				return eff;
		}
		return null;
}

function f_removeCenterPointFx(layer){
		var centerP=f_findFxName(layer,fxName_src1,"ADBE Point Control");
		if(centerP) centerP.remove();
}

function f_getCenterValue(layer){
		var centerP=f_findFxName(layer,fxName_src1,"ADBE Point Control");
		if(!centerP){
				throw new Error("ポイント制御が見つかりません。「1. 中心指定 追加」を押してください。");
		}

		if( !centerP.property(1) ){
				throw new Error("ポイント制御のvalueを取得できません。");
		}

		return centerP.property(1).value;
}

function f_addCenterPoint(){
	app.beginUndoGroup("割れの中心用ポイント制御を追加");

	var comp=app.project.activeItem;
	if(!(comp && comp instanceof CompItem)){
			throw new Error("アクティブなコンポジションを開いてください。");
	}

	if(comp.selectedLayers.length!==1){
			throw new Error("レイヤーを1つだけ選択してください。");
	}

	var layer=comp.selectedLayers[0];
	var centerP=f_getOrAddEffect(layer, fxName_src1, "ADBE Point Control", false);

	layer.selected=true;

	try{
			if(centerP.property(1)){
					centerP.property(1).selected=true;
			}
	}catch(e){}

	app.endUndoGroup();
}

// -----エフェクトがなければ追加-----
function f_getOrAddEffect(layer, effectName, matchName, setInitialValue, initialValue){
	var effects=f_getEffectsGroup(layer);
	if(!effects) throw new Error("エフェクトグループを取得できません。");

	var existingEffect=f_findFxName(layer, effectName, matchName);
	if(existingEffect) return existingEffect;

	var newEffect=effects.addProperty(matchName);
	if(!newEffect) throw new Error(matchName+"を追加できませんでした。");

	newEffect.name=effectName;

	if(setInitialValue) newEffect.property(1).setValue(initialValue);

	return newEffect;
}

function f_drawMask(layer,verts,lockMask){
		verts=f_deduplicate(verts);
		if(verts.length < 3) return null;

		var maskGr=f_getMaskGroup(layer);
		if (!maskGr) throw new Error("マスクグループを取得できません。");

		var addMask=maskGr.addProperty("ADBE Mask Atom");
		if (!addMask) throw new Error("マスクを追加できませんでした。");

		var shp=new Shape();
			shp.vertices=verts;

		var inTs=[], outTs=[];
		for (var i=0;i<verts.length;i++){
				inTs.push([0, 0]);
				outTs.push([0, 0]);
		}

			shp.inTangents=inTs;
			shp.outTangents=outTs;
			shp.closed=true;

		addMask.property("ADBE Mask Shape").setValue(shp);

		var maskModeProp=addMask.property("ADBE Mask Mode");
		if (maskModeProp) maskModeProp.setValue(MaskMode.ADD);

		if (lockMask){
				addMask.locked=true;
		}

		return addMask;
}


// ==================================================
// 境界計算系
// ==================================================

// 中心から伸びる線と、矩形の最初の交点を計算する
function f_lineRectIntersection(cx,cy,dx,dy,left,top,right,bottom){
		var pts=[];
		var t,x,y;

		if (dx !== 0){
				t=(left-cx)/dx;
				y=cy+t*dy;
				if (t>0 && y>=top && y<=bottom) pts.push([left,y,t]);

				t=(right-cx)/dx;
				y=cy+t*dy;
				if(t>0 && y>=top && y<=bottom) pts.push([right,y,t]);
		}

		if (dy!==0){
				t=(top-cy)/dy;
				x=cx+t*dx;
				if(t>0 && x>=left && x<=right) pts.push([x,top,t]);

				t=(bottom-cy)/dy;
				x=cx+t*dx;
				if(t>0 && x>=left && x<=right) pts.push([x,bottom,t]);
		}

		if(pts.length === 0) return [cx,cy];
		pts.sort(function(a,b) { return a[2]-b[2]; });
		return [pts[0][0], pts[0][1]];
}

var BorderUtil={
		sideIndex: function(p,left,top,right,bottom){
				var eps=0.001;
				if (Math.abs(p[1]-top) < eps) return 0;
				if (Math.abs(p[0]-right) < eps) return 1;
				if (Math.abs(p[1]-bottom) < eps) return 2;
				if (Math.abs(p[0]-left) < eps) return 3;
				return -1;
		},

		cornerByIndex: function(idx,left,top,right,bottom){
				if(idx === 0) return [left,top];
				if(idx === 1) return [right,top];
				if(idx === 2) return [right,bottom];
				return [left,bottom];
		},

		pathCW: function(pA,pB,left,top,right,bottom){
				var sA=this.sideIndex(pA,left,top,right,bottom);
				var sB=this.sideIndex(pB,left,top,right,bottom);

				if (sA<0 || sB<0) return [pA.slice(),pB.slice()];

				var pts=[pA.slice()];
				var s=sA;

				while (s!==sB){
						pts.push(this.cornerByIndex( (s+1)%4, left,top,right,bottom) );
						s=(s+1)%4;
				}

				pts.push( pB.slice() );
				return pts;
		},

		length: function(path){
				var total=0;
				for (var i=0;i<path.length-1;i++){
						var dx=path[i+1][0]-path[i][0];
						var dy=path[i+1][1]-path[i][1];
						total+=Math.sqrt(dx*dx+dy*dy);
				}
				return total;
		},

		pointAt: function(path,t){
				if(path.length === 1) return path[0].slice();
				if(path.length === 2){
						return [
								f_lerp(path[0][0],path[1][0],t),
								f_lerp(path[0][1],path[1][1],t)
						];
				}

				var total=this.length(path);
				if (total<= 0) return path[0].slice();

				var target=total*t;
				var acc=0;

				for (var i=0;i<path.length-1;i++){
						var dx=path[i+1][0]-path[i][0];
						var dy=path[i+1][1]-path[i][1];
						var len=Math.sqrt(dx*dx+dy*dy);

						if(target<=acc+len || i===path.length-2){
								var localT=(target-acc)/len;
								if ( !isFinite(localT) ) localT=0;
								return [
										f_lerp(path[i][0],path[i+1][0],localT),
										f_lerp(path[i][1],path[i+1][1],localT)
								];
						}
						acc+=len;
				}

				return path[path.length-1].slice();
		}
};

// 内側の2点と外側の2点を調べて、マスク用のポリゴンを求める
function f_buildOuterPiece(innerA,innerB,edgeB,edgeA,left,top,right,bottom){
		var cw=BorderUtil.pathCW(edgeB,edgeA,left,top,right,bottom);
		var cwRe=f_reverseVerts( BorderUtil.pathCW(edgeA,edgeB,left,top,right,bottom) );

		var verts1=[innerA,innerB].concat(cw);
		var verts2=[innerA,innerB].concat(cwRe);

			verts1=f_deduplicate(verts1);
			verts2=f_deduplicate(verts2);

		var area1=Math.abs(f_polygonArea(verts1));
		var area2=Math.abs(f_polygonArea(verts2));

		return (area1<area2)?verts1:verts2;
}


// ==================================================
// レイヤー生成
// ==================================================

function f_createPieceLayer(src,pieceIndex,verts,sourceLayerName,lockMask){
		var layer=src.duplicate();
			layer.threeDLayer=true;
			layer.name=src.name+"_"+pieceIndex;

		f_removeCenterPointFx(layer);
		f_getOrAddEffect(layer, fxName_dup1, "ADBE Slider Control", true, 0);

		var maskGr=f_getMaskGroup(layer);
		if(!maskGr) return;

		for (var i=maskGr.numProperties;i>=1;i--){
				var m=maskGr.property(i);
				if(m) m.remove();
		}

		f_drawMask(layer,verts,lockMask);

		if (verts && verts.length>0){
				var centroid=f_maskCentroid(verts);
				var maskCenterX=centroid[0];
				var maskCenterY=centroid[1];

				var oldAnchor=layer.anchorPoint.value;
				var oldPos=layer.position.value;
				var scale=layer.scale.value;

				var dx=(maskCenterX-oldAnchor[0])*(scale[0]/100);
				var dy=(maskCenterY-oldAnchor[1])*(scale[1]/100);

				layer.anchorPoint.setValue([maskCenterX,maskCenterY]);

				if(oldPos.length>=3){
						layer.position.setValue([
								oldPos[0]+dx,
								oldPos[1]+dy,
								oldPos[2]
						]);
				}else{
						layer.position.setValue([
								oldPos[0]+dx,
								oldPos[1]+dy
						]);
				}
		}

		layer.property("ADBE Transform Group").property("ADBE Position").expression=[
				'var lyr0=thisComp.layer("' + sourceLayerName + '");',
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var center=lyr0.effect("' + fxName_src1 + '")("ポイント");',
				'var slide=effect("' + fxName_dup1 + '")(1);',
				'var allslide=lyr1.effect("' + fxName_src2 + '")(1);',
				'\r',
				'var angle=value-center;',
				'var nullVal=lyr1.position-['+String(centerNull.position.value)+'];',
				'\r',
				'if(length(angle)<0.001){',
				'		value;',
				'}else{',
				'		value+nullVal+angle/100*(slide+allslide);',
				'}'
		].join("\r");

		layer.property("ADBE Transform Group").property("ADBE Rotate X").expression=[
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var allRotate=lyr1.effect("' + fxName_src3 + '")(1);',
				'var rand=wiggle(0,lyr1.effect("' + fxName_src6 + '")(1));',
				'\r',
				'value+rand+allRotate;'
		].join("\r");

		layer.property("ADBE Transform Group").property("ADBE Rotate Y").expression=[
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var allRotate=lyr1.effect("' + fxName_src4 + '")(1);',
				'var rand=wiggle(0,lyr1.effect("' + fxName_src6 + '")(1));',
				'\r',
				'value+rand+allRotate;'
		].join("\r");

		layer.property("ADBE Transform Group").property("ADBE Rotate Z").expression=[
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var allRotate=lyr1.effect("' + fxName_src5 + '")(1);',
				'var rand=wiggle(0,lyr1.effect("' + fxName_src6 + '")(1));',
				'\r',
				'value+rand+allRotate;'
		].join("\r");

		return layer;
}

function f_smartNuller(){
		var addLyr=app.project.activeItem.layers.addNull(); // nullレイヤー追加

		for(i=1;i<=app.project.items.length;i++){
				//[平面]か[ヌル]であることを判別
				if( (app.project.items[i].mainSource == "[object SolidSource]")&&(app.project.items[i].name=="ヌル 1") ){
						addLyr.replaceSource(app.project.items[i],true);
				break;
				}
		}

		for(i=app.project.items.length;i>=1;i--){
				//使用されていない[ヌル n]のみ削除
				if( (app.project.items[i].mainSource == "[object SolidSource]")&&(app.project.items[i].name.indexOf("ヌル")!=-1)&&(app.project.items[i].name!="ヌル 1")&&(app.project.items[i].usedIn=="") ){
						app.project.items[i].remove();
				}
		}
		return addLyr;
}


// ==================================================
// メイン処理
// ==================================================

function f_crackMask(angleSplit,radialSplit,edgeSplit,angleJitter,radialJitter,rayJitter,lockMask){
		app.beginUndoGroup("crackMask");

		var comp=app.project.activeItem;
		if( !(comp && comp instanceof CompItem) ){
				throw new Error("アクティブなコンポジションを開いてください。");
		}

		if(comp.selectedLayers.length!==1){
				throw new Error("レイヤーを1枚だけ選択してください。");
		}

		var src=comp.selectedLayers[0];
		var sourceLayerName=src.name+"_maskSource";

		var rect=f_getLayerRect(src,comp.time);
		if (!rect) throw new Error("レイヤー範囲を取得できませんでした。");

		var left=rect.left;
		var top=rect.top;
		var right=rect.left+rect.width;
		var bottom=rect.top+rect.height;

		var centerPt=f_getCenterValue(src);
		var cx=centerPt[0];
		var cy=centerPt[1];

		nullLyr1=f_smartNuller();
		nullLyr1.name="crackMask_Control";
		nullLyr1.label=11;//オレンジ

		centerNull = nullLyr1;
		nullLyr1.selected=true;

		var baseAngles=[];
		var i,j;

		for(i=0;i<angleSplit;i++){
				baseAngles.push( (Math.PI*2/angleSplit)*i+f_rand(-angleJitter,angleJitter) );
		}
		baseAngles.sort(function(a,b) { return a-b; });

		var edgePoints=[];
		for (i=0;i<baseAngles.length;i++){
				var a=baseAngles[i];
				var dx=Math.cos(a);
				var dy=Math.sin(a);
				var hit=f_lineRectIntersection(cx,cy,dx,dy,left,top,right,bottom);

				edgePoints.push({
						x: hit[0],
						y: hit[1]
				});
		}

		var rayOffsets=[];
		for (i=0;i<baseAngles.length;i++){
				rayOffsets[i]=[];
				rayOffsets[i][0]=0;
				rayOffsets[i][radialSplit]=0;

				for(j=1;j<radialSplit;j++){
						rayOffsets[i][j]=f_rand(-rayJitter,rayJitter);
				}
		}

		var rings=[];
		for (j=0;j<=radialSplit;j++){
				rings[j]=[];

				for (i=0;i<baseAngles.length;i++){
						var baseA=baseAngles[i];
						var a2=baseA+rayOffsets[i][j];

						var dx2=Math.cos(a2);
						var dy2=Math.sin(a2);
						var hit2=f_lineRectIntersection(cx,cy,dx2,dy2,left,top,right,bottom);

						var vx=hit2[0]-cx;
						var vy=hit2[1]-cy;

						var t=j/radialSplit;

						if(j === radialSplit){
								rings[j][i]=[hit2[0],hit2[1]];
								continue;
						}

						var rScale=t;
						if(j>0){
								rScale+=f_rand(-radialJitter/radialSplit, radialJitter/radialSplit);
						}
						rScale=f_clamp(rScale, 0, 0.999);

						var px=cx+vx*rScale;
						var py=cy+vy*rScale;

						px=f_clamp(px,left,right);
						py=f_clamp(py,top,bottom);

						rings[j][i]=[px,py];
				}
		}

		var pieceIndex=1;

		for (j=0;j<radialSplit-1;j++){
				for(i=0;i<baseAngles.length;i++){
						var i2=(i+1)%baseAngles.length;

						var v1=rings[j][i];
						var v2=rings[j][i2];
						var v3=rings[j+1][i2];
						var v4=rings[j+1][i];

						var verts;
						if(j===0){
								verts=[
										[cx,cy],
										v3,v4
								];
						}else{
								verts=[
										v1,v2,v3,v4
								];
						}

						f_createPieceLayer(src,pieceIndex,verts,sourceLayerName,lockMask);
						pieceIndex++;
				}
		}

		var outerRingIndex=radialSplit-1;

		for (i=0;i<baseAngles.length;i++){
				var next=(i+1)%baseAngles.length;

				var innerA=rings[outerRingIndex][i];
				var innerB=rings[outerRingIndex][next];
				var edgeA=[edgePoints[i].x,edgePoints[i].y];
				var edgeB=[edgePoints[next].x,edgePoints[next].y];

				var border_cw=BorderUtil.pathCW(edgeA,edgeB,left,top,right,bottom);
				var border_cwRe=f_reverseVerts( BorderUtil.pathCW(edgeB,edgeA,left,top,right,bottom) );

				var useCW=BorderUtil.length(border_cw) <= BorderUtil.length(border_cwRe);
				var borderPath=useCW?border_cw:border_cwRe;

				var prevInner=innerA.slice();
				var prevEdge=borderPath[0].slice();

				for(j=1;j<=edgeSplit;j++){
						var t2=j/edgeSplit;

						var currInner, currEdge;
						if(j===edgeSplit){
								currInner=innerB.slice();
								currEdge=borderPath[borderPath.length-1].slice();
						}else{
								currInner=[
										f_lerp(innerA[0],innerB[0],t2),
										f_lerp(innerA[1],innerB[1],t2)
								];
								currEdge=BorderUtil.pointAt(borderPath, t2);
						}

						var vertsOuter=f_buildOuterPiece(
								prevInner,
								currInner,
								currEdge,
								prevEdge,
								left,top,right,bottom
						);

						f_createPieceLayer(src,pieceIndex,vertsOuter,sourceLayerName,lockMask);
						pieceIndex++;

						prevInner=currInner.slice();
						prevEdge=currEdge.slice();
				}
		}

		src.enabled=false;
		src.name=src.name+"_maskSource";
		f_getOrAddEffect(centerNull, fxName_src2, "ADBE Slider Control", true, 0);
		f_getOrAddEffect(centerNull, fxName_src3, "ADBE Angle Control", true, 0);
		f_getOrAddEffect(centerNull, fxName_src4, "ADBE Angle Control", true, 0);
		f_getOrAddEffect(centerNull, fxName_src5, "ADBE Angle Control", true, 0);
		f_getOrAddEffect(centerNull, fxName_src6, "ADBE Angle Control", true, 0);

		app.endUndoGroup();
}


// ==================================================
// 起動
// ==================================================

function f_showDialog(){
	var win=buildUI();
	if(win instanceof Window){
			win.layout.layout(true);
			win.show();
	}else{
			win.layout.layout(true);
	}
}

var wobj=new f_showDialog();

UIの準備

JavaScript
function buildUI(thisObj){
		var pnl=(thisObj instanceof Panel)
				? thisObj
				: new Window("palette",scName,undefined,{resizeable: true});

		if(!pnl)return pnl;

		pnl.orientation="column";
		pnl.alignChildren=["fill", "top"];
		pnl.spacing=8;
		pnl.margins=12;

		var btn1=pnl.add("group");
			btn1.orientation="row";
			btn1.alignChildren=["fill", "center"];
			btn1.spacing=8;

		var addPointBtn=btn1.add("button",undefined,"1. 中心指定 追加");
			addPointBtn.onClick=function(){
				try{
						f_addCenterPoint();
				}catch(e){
						alert( e.toString() );
				}
		};

		var setArea=pnl.add("panel",undefined,"2. 設定");
			setArea.orientation="column";
			setArea.alignChildren=["fill", "top"];
			setArea.margins=10;
			setArea.spacing=2;

		var input_angleSplit	=f_setInput(setArea,"角度分割", "12");
		var input_angleJitter	=f_setInput(setArea," ┗角度ランダム変数", "0.08");
		var input_radialSplit	=f_setInput(setArea,"内→外分割", "3");
		var input_radialJitter=f_setInput(setArea," ┗内→外角度ランダム変数", "0.10");
		var input_edgeSplit		=f_setInput(setArea,"端の調整分割", "2");
		var input_rayJitter		=f_setInput(setArea,"同心円崩れ", "0.18");

		var chk_lockMask=setArea.add("checkbox", undefined, "分割後のマスクをロック");
			chk_lockMask.value=true;

		var btn2=pnl.add("group");
			btn2.orientation="row";
			btn2.alignChildren=["fill", "center"];

		var runBtn=btn2.add("button", undefined, "3. Crack!!");
			runBtn.onClick=function(){
				try{
						var angleSplit	=parseInt(input_angleSplit.text, 10);
						var radialSplit =parseInt(input_radialSplit.text, 10);
						var edgeSplit		=parseInt(input_edgeSplit.text, 10);
						var angleJitter =parseFloat(input_angleJitter.text);
						var rayJitter		=parseFloat(input_rayJitter.text);
						var radialJitter=parseFloat(input_radialJitter.text);

						if (isNaN(angleSplit) || angleSplit < 3) throw new Error("角度分割は3以上にしてください。");
						if (isNaN(angleJitter) || angleJitter < 0) throw new Error("┗角度ランダム変数は0以上にしてください。");
						if (isNaN(radialSplit) || radialSplit < 1) throw new Error("内→外分割は1以上にしてください。");
						if (isNaN(rayJitter) || rayJitter < 0) throw new Error("┗内→外角度ランダム変数は0以上にしてください。");
						if (isNaN(edgeSplit) || edgeSplit < 1) throw new Error("端の調整分割は1以上にしてください。");
						if (isNaN(radialJitter) || radialJitter < 0) throw new Error("同心円崩れは0以上にしてください。");

						f_crackMask(
								angleSplit,
								radialSplit,
								edgeSplit,
								angleJitter,
								radialJitter,
								rayJitter,
								chk_lockMask.value
						);

				}catch(e){
						alert( e.toString() );
				}
		};

		pnl.layout.layout(true);
		pnl.layout.resize();
		pnl.onResizing=pnl.onResize=function(){ this.layout.resize(); };

		return pnl;
}

function f_setInput(parent,label,val){
		var gr_label=parent.add("group");
			gr_label.orientation="row";
			gr_label.alignChildren=["left", "center"];
			gr_label.spacing=8;

		var tx_label=gr_label.add("statictext", undefined, label);
			tx_label.preferredSize=[160, -1];

		var gr_edit=gr_label.add("group");
			gr_edit.orientation="row";
			gr_edit.alignChildren=["right", "center"];
			gr_edit.spacing=0;

		var tx_edit=gr_edit.add("edittext", undefined, val);
			tx_edit.characters=5;
			tx_edit.preferredSize=[60, -1];

		return tx_edit;
}

buildUI()関数で

・ポイント制御を追加するボタン、
・設定類
・実行ボタン

を準備。

まず何を押すかを一目で理解できる構成にしました。

ひび割れの中心は数値入力では狙った場所を指定するのに苦労するため、マウスドラッグで指定できるよう、ポイント制御エフェクトを選びました。「割れの中心」にリネームします。ヌルレイヤーと悩みましたが、全体を放射状に移動できるように仕込みたかったので実行後は簡単に動かしづらい仕掛けにしたいためポイント制御での指定にしました。

実行ボタンである[3.Crack!!]ボタンをクリックすると、runBtn.onClickが呼び出され、[2.設定]内の数値を取り込んでいきます。

ひび割れの計算が破綻する数値で処理しないよう、最小値、最大値のリミッターをある程度決めてあります。リミッター外の範囲だったらアラートを出すようにしました。

使用者が間違えて未入力やNaN(数値ではない文字)を入力してもクラッシュしない気遣いです。

幾何計算の肝

JavaScript
function f_rand(min,max){
		return min+Math.random()*(max-min);
}

function f_clamp(v,min,max){
		return Math.max( min,Math.min(max,v) );
}

// 「2点の間を t パーセントで区切って、途中の座標を決める」ために使う式
// 境界線上に点を等分割するのに使う
function f_lerp(a,b,t){
		return a+(b-a)*t;
}

// f_drawMask内部で、連続した頂点のうち、「ほとんど同じ座標の点」を削除
function f_organize(p){
		return Math.round(p[0]*1000)+"_"+Math.round(p[1]*1000);
}

// f_organizeと合わせて、閉じたマスク用に不要な、最初と最後が同じ点を除外
function f_deduplicate(verts){
		var out=[];
		var prev=null;
		for (var i=0;i<verts.length;i++){
				var p=verts[i];
				if( !prev || f_organize(prev) !== f_organize(p) ){
						out.push(p);
						prev=p;
				}
		}
		if( out.length>1 && f_organize(out[0]) === f_organize(out[out.length-1]) ){
				out.pop();
		}
		return out;
}

// 多角形の面積を求める有名な公式とのこと
function f_polygonArea(verts){
		var a=0;
		for(var i=0;i<verts.length;i++){
				var p1=verts[i];
				var p2=verts[(i+1)%verts.length];
				a+=p1[0]*p2[1]-p2[0]*p1[1];
		}
		return a*0.5;
}

rand / clamp / lerpが、幾何計算の基礎を担う関数です。

rand(min, max)
指定範囲内のランダム値を返すだけですが、jitter や rayJitter といった「ランダム変数」に使用します。

clamp(v, min, max)
乱雑さをコントロールするときに0〜1の範囲内に収めるなど、値を意図した範囲に保つのに役立ちます。

lerp(a, b, t)
直線補間を実装しており、線分上の任意の点を求める用に使います。

そしてf_organize()とf_deduplicate()で微妙にズレたほとんど同じ位置の頂点をキレイにします。割れた欠片は閉じたマスクパスで描き、ピタッと合わさらなければならないので、不要な点をf_organize()内でMath.round()で丸めてf_deduplicate()で比較します。

170行目のf_polygonArea()で

幾何関数

JavaScript
// 中心から伸びる線と、矩形の最初の交点を計算する
function f_lineRectIntersection(cx,cy,dx,dy,left,top,right,bottom){
		var pts=[];
		var t,x,y;

		if (dx !== 0){
				t=(left-cx)/dx;
				y=cy+t*dy;
				if (t>0 && y>=top && y<=bottom) pts.push([left,y,t]);

				t=(right-cx)/dx;
				y=cy+t*dy;
				if(t>0 && y>=top && y<=bottom) pts.push([right,y,t]);
		}

		if (dy!==0){
				t=(top-cy)/dy;
				x=cx+t*dx;
				if(t>0 && x>=left && x<=right) pts.push([x,top,t]);

				t=(bottom-cy)/dy;
				x=cx+t*dx;
				if(t>0 && x>=left && x<=right) pts.push([x,bottom,t]);
		}

		if(pts.length === 0) return [cx,cy];
		pts.sort(function(a,b) { return a[2]-b[2]; });
		return [pts[0][0], pts[0][1]];
}

先に、本体の幾何処理です。

クモの巣をイメージしてもらうと、中心から放射状に伸びる線と、同心円が合わさっているだけです。これをランダムに揺らせば自然に見えます。

この縦と横の線が交わる点が分かれば、マスクの形が描けそうです。

f_lineRectIntersection()で中心点から射線を出し、矩形(レイヤーの端っこ)との交点を求めます。
dx, dy は射線の方向ベクトル、tをパラメータとして、left/right か top/bottomのどちらかで交点があればpts.pushします。

ただ、「交点は複数ある場合がある」のです。これを理解するのがキツかった。

tでソートして、t>0で最初に当たる点を返す関数を作りました。この関数により、「中心から放射線を引いたときに、レイヤーのどの辺に当たるか」が判定できるようになりました。

JavaScript
var BorderUtil={
		sideIndex: function(p,left,top,right,bottom){
				var eps=0.001;
				if (Math.abs(p[1]-top) < eps) return 0;
				if (Math.abs(p[0]-right) < eps) return 1;
				if (Math.abs(p[1]-bottom) < eps) return 2;
				if (Math.abs(p[0]-left) < eps) return 3;
				return -1;
		},
// なっがいので省略
JavaScript
// 多角形の面積を求める有名な公式とのこと
function f_polygonArea(verts){
		var a=0;
		for(var i=0;i<verts.length;i++){
				var p1=verts[i];
				var p2=verts[(i+1)%verts.length];
				a+=p1[0]*p2[1]-p2[0]*p1[1];
		}
		return a*0.5;
}
JavaScript
// 内側の2点と外側の2点を調べて、マスク用のポリゴンを求める
function f_buildOuterPiece(innerA,innerB,edgeB,edgeA,left,top,right,bottom){
		var cw=BorderUtil.pathCW(edgeB,edgeA,left,top,right,bottom);
		var cwRe=f_reverseVerts( BorderUtil.pathCW(edgeA,edgeB,left,top,right,bottom) );

		var verts1=[innerA,innerB].concat(cw);
		var verts2=[innerA,innerB].concat(cwRe);

			verts1=f_deduplicate(verts1);
			verts2=f_deduplicate(verts2);

		var area1=Math.abs(f_polygonArea(verts1));
		var area2=Math.abs(f_polygonArea(verts2));

		return (area1<area2)?verts1:verts2;
}

BorderUtil(長いので抽出は省略)は「長方形の枠をグラフのように扱い、二点の間の枠沿いの最短経路やその途中座標を取得する」ための補助的な値を細々と取得していっています。485行目からのポリゴンを作るところなどで使っていきます。

矩形の外周を「時計回りのパス」として配列で保持し、パス全体の長さを計算 → その割合でポイントを取得する流れです。

496と497行目、f_polygonArea()で、外周を分割するときの「どこからどこまでを1ピースにするか」を、長さの比率で判別しました。時計回りと反時計回りでパスの面積は同じじゃないか、と思うのですが、+と-で面積は大きく変わってしまうのです。マスクをイメージしていただくと、時計回りのパスと反時計回りのパスというのが、「パスで囲った小さいエリア」と、そのマスクを反転した「くり抜かれた部分がある外側の領域」とに分かれて扱われるので、面積が小さい方が欠片として使いたいエリアです。

f_buildOuterPiece()で内側の2点と外周の2点から、矩形のどちら側のパスかを選び、短い方にしたマスク頂点を配列で返します。
内側の4点や外周のパスをつなぎ、面積でどちらかを選択して、一応マスクがひっくり返らないようにしています。

図形の頂点配列をAfter Effectsで扱えるパスの頂点として変換する良い頭の体操になりました(もうやりたくない)。

マスクの処理(Aeの操作部分)

221行目からに戻ります。

JavaScript
function f_getMaskGroup(layer){
		if(!layer) return null;
		return layer.property("ADBE Mask Parade");
}

function f_getEffectsGroup(layer){
		if(!layer) return null;
		return layer.property("ADBE Effect Parade");
}

function f_getLayerRect(layer,time){
		var sr=null;

		try{
				sr=layer.sourceRectAtTime(time,false);
		}catch(e){}

		if(sr && sr.width>0 && sr.height>0){
				return{
						left: sr.left,
						top: sr.top,
						width: sr.width,
						height: sr.height
				};
		}

		if(layer.width && layer.height){
				return {
						left: -layer.width/2,
						top: -layer.height/2,
						width: layer.width,
						height: layer.height
				};
		}

		return null;
}

f_getMaskGroup()、f_getEffectsGroup()はマスクかエフェクトのグループを取得するだけ。万が一これらが作られてなかったり、消してしまった際に処理を止める用。

f_getLayerRect(layer,time)でソースレイヤーの矩形を調べて、不規則な形でうまく取得できなかったり幅や高さが0以下だった場合は代わりに自身のwidthとheightを基準に見た目通りの範囲を取得しようとします。

JavaScript
function f_findFxName(layer,name,matchName){
		var effects=f_getEffectsGroup(layer);
		if (!effects) return null;

		for(var i=1;i<=effects.numProperties;i++){
				var eff=effects.property(i);
				if(!eff) continue;
				if(name && eff.name !== name) continue;
				if(matchName && eff.matchName !== matchName) continue;
				return eff;
		}
		return null;
}

function f_removeCenterPointFx(layer){
		var centerP=f_findFxName(layer,fxName_src1,"ADBE Point Control");
		if(centerP) centerP.remove();
}

function f_getCenterValue(layer){
		var centerP=f_findFxName(layer,fxName_src1,"ADBE Point Control");
		if(!centerP){
				throw new Error("ポイント制御が見つかりません。「1. 中心指定 追加」を押してください。");
		}

		if( !centerP.property(1) ){
				throw new Error("ポイント制御のvalueを取得できません。");
		}

		return centerP.property(1).value;
}

f_findFxName()は一応作ろうとしているエフェクトと競合するものが既にあったら新たに作らず、なければ追加するためにサーチする用です。

ソースレイヤーを複製してマスクを描画、とする仕様にしたため、中心点のエフェクト制御ごと複製されるので、複製後にf_removeCenterPointFx()で消します。もうちょいスマートに出来たかも。

マスクで描画するひび割れは、中心点を基準にするので、f_getCenterValue()でポイント制御の値を取得します。

マスク描画のメイン処理

JavaScript
function f_drawMask(layer,verts,lockMask){
		verts=f_deduplicate(verts);
		if(verts.length < 3) return null;

		var maskGr=f_getMaskGroup(layer);
		if (!maskGr) throw new Error("マスクグループを取得できません。");

		var addMask=maskGr.addProperty("ADBE Mask Atom");
		if (!addMask) throw new Error("マスクを追加できませんでした。");

		var shp=new Shape();
			shp.vertices=verts;

		var inTs=[], outTs=[];
		for (var i=0;i<verts.length;i++){
				inTs.push([0, 0]);
				outTs.push([0, 0]);
		}

			shp.inTangents=inTs;
			shp.outTangents=outTs;
			shp.closed=true;

		addMask.property("ADBE Mask Shape").setValue(shp);

		var maskModeProp=addMask.property("ADBE Mask Mode");
		if (maskModeProp) maskModeProp.setValue(MaskMode.ADD);

		if (lockMask){
				addMask.locked=true;
		}

		return addMask;
}
JavaScript
function f_createPieceLayer(src,pieceIndex,verts,sourceLayerName,lockMask){
		var layer=src.duplicate();
			layer.threeDLayer=true;
			layer.name=src.name+"_"+pieceIndex;

// ~いろいろあって~

		layer.property("ADBE Transform Group").property("ADBE Rotate X").expression=[
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var allRotate=lyr1.effect("' + fxName_src3 + '")(1);',
				'var rand=wiggle(0,lyr1.effect("' + fxName_src6 + '")(1));',
				'\r',
				'value+rand+allRotate;'
		].join("\r");

		layer.property("ADBE Transform Group").property("ADBE Rotate Y").expression=[
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var allRotate=lyr1.effect("' + fxName_src4 + '")(1);',
				'var rand=wiggle(0,lyr1.effect("' + fxName_src6 + '")(1));',
				'\r',
				'value+rand+allRotate;'
		].join("\r");

		layer.property("ADBE Transform Group").property("ADBE Rotate Z").expression=[
				'var lyr1=thisComp.layer("' + centerNull.name + '");',
				'var allRotate=lyr1.effect("' + fxName_src5 + '")(1);',
				'var rand=wiggle(0,lyr1.effect("' + fxName_src6 + '")(1));',
				'\r',
				'value+rand+allRotate;'
		].join("\r");

		return layer;
}
JavaScript
function f_crackMask(angleSplit,radialSplit,edgeSplit,angleJitter,radialJitter,rayJitter,lockMask){
// 以下省略

507行目からソースレイヤーを複製→ポイント制御を削除→個別にスライドする用の「外側にオフセット」エフェクト追加→該当のマスク描画→全体を移動、回転するエクスプレッションの仕込み処理の本体です。

622行目、f_crackMask()で上記の本体処理含め、実行します。

元のレイヤーは非表示+レイヤー名末尾に”_maskSource”のリネームで完了です。

ダウンロード

11.ショットサイズ/フレーミング;撮影ワークフロー前のページ

ピックアップ記事

  1. YouTubeで一時停止中のコントローラーを非表示にするブックマークレット

  2. なぜ?After Effectsの操作を「スクリプト」で効率化

  3. なぜ?After Effectsのレイヤーをエクスプレッションで効率化

  4. amazonのスポンサー商品(広告)を非表示にするブックマークレット「amazO…

  5. フリーランスの開業届提出は開業freeeでとにかく簡単に

関連記事

  1. スクリプト

    2つのレイヤー間に他レイヤーを整列するスクリプト「pos2LayersAlign.jsx」

    root(はじめに選んだ)レイヤーとgoal(最後に選んだ)レイヤーを…

  2. スクリプト

    アイソメトリックビューを簡易的に実現するスクリプト「isometricCamera.jsx」

    インフォグラフィックスにも相性のいいアイソメトリックビュー風カメラを手…

  3. スクリプト

    After Effectsのスクリプトの書き方

    After Effectsのスクリプトを作るのに必要なものを紹介します…

  4. スクリプト

    tRemapSelectorSetterした2つのリマップ番号を入れ替えるスクリプト「numSlid…

    「タイムリマップでレイヤー管理するスクリプト(フリーズフレーム)「tR…

コメント

  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

CAPTCHA


PAGE TOP