年末年始につくったものまとめ
あけましておめでとうございます。
本年もよろしくお願いいたします。
年末年始はのびのびと手を動かしていました。
前から気になっていたシェーダースケッチに挑戦できたので良かったです。やってみたら数式だけでいろいろな図形があらわれるのが楽しくて、ストレス解消にもなる気がしたので、今後ものびのび続けていきたいと思います。
12月28日
さっそく帰省の新幹線の中でシェーダーかいてました。
空間の歪めかた分かってきました
— setchi (@setchi) 2017年12月28日
(新幹線で楽しくシェーダー勉強してます) pic.twitter.com/oyKSAV9sSt
12月29日
ブログ更新しました!#はてなブログ #unity3d 【Unity】空間を歪めてポータルのようなエフェクトをつくる - setchi's https://t.co/gHDuHK9YJX
— setchi (@setchi) 2017年12月29日
サクッと試せるように最小構成のプロジェクトも用意しました。
今後も何か作ったらここにアップしていくと思います。
github.com
12月30日
この日からはシェーダーでのびのびとお絵描きしていました。
リポジトリはこちらです。
github.com
上記リポジトリを WebGL ビルドしたものです。
Unity WebGL Player | Unity-ShaderSketches
Unity-ShaderSketches/TransformGrid.shader at master · setchi/Unity-ShaderSketches · GitHub
12月31日
Unity-ShaderSketches/TransformTriangle.shader at master · setchi/Unity-ShaderSketches · GitHub
Unity-ShaderSketches/Triangle2.shader at master · setchi/Unity-ShaderSketches · GitHub
Unity-ShaderSketches/Triangle3.shader at master · setchi/Unity-ShaderSketches · GitHub
1月1日
Unity-ShaderSketches/TransformTriangle2.shader at master · setchi/Unity-ShaderSketches · GitHub
Unity-ShaderSketches/Circle1.shader at master · setchi/Unity-ShaderSketches · GitHub
Unity-ShaderSketches/Lattice1.shader at master · setchi/Unity-ShaderSketches · GitHub
Unity-ShaderSketches/Lattice1.shader at master · setchi/Unity-ShaderSketches · GitHub
1月2日
Unity-ShaderSketches/Lattice2.shader at master · setchi/Unity-ShaderSketches · GitHub
Unity-ShaderSketches/Circle2.shader at master · setchi/Unity-ShaderSketches · GitHub
1月3日
Unity-ShaderSketches/Voronoi1.shader at master · setchi/Unity-ShaderSketches · GitHub
このあたりから極座標を使った模様作りを覚えました
Unity-ShaderSketches/Snow1.shader at master · setchi/Unity-ShaderSketches · GitHub
【Unity】空間を歪めてポータルのようなエフェクトをつくる
はじめに
帰省の新幹線の中でこういうポストプロセスエフェクトを作ったので、作り方を紹介します。
空間の歪めかた分かってきました
— setchi (@setchi) 2017年12月28日
(新幹線で楽しくシェーダー勉強してます) pic.twitter.com/oyKSAV9sSt
実装方針
ポータルの中心から一定距離内のピクセル(緑色の範囲)を外側に圧縮して、広がった分だけ内側に違う絵を描画します。
実装
ShaderLab
Shader "Custom/Portal" { Properties { _MainTex ("MainTex", 2D) = "white"{} } CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _SubTex; float _Aspect; float _Radius; float2 _Position; float4 frag(v2f_img i) : SV_Target { float width = 0.07; // 自身のピクセルからポータル中心までの距離 float distance = length((_Position - i.uv) * float2(1, _Aspect)); // 自身のピクセル位置での歪み具合 float distortion = 1 - smoothstep(_Radius - width, _Radius, distance); // 自身のピクセル位置での歪み具合分だけ // ポータル中心の方へずらした uv を計算します float uv = i.uv + (_Position - i.uv) * distortion; // 計算した uv で _MainTex のカラーを出力します // ポータル内に違う絵を出すために、 // lerp + step で出力テクスチャを切り替えています return lerp(tex2D(_MainTex, uv), tex2D(_SubTex, i.uv), step(1, distortion)); } ENDCG SubShader { Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag ENDCG } } }
カメラ用C#スクリプト
マウスダウンでマウス位置にポータルが開くように、シェーダのプロパティを操作しています。
using DG.Tweening; using UnityEngine; public class Portal : MonoBehaviour { [SerializeField] Material material; [SerializeField] Texture texture; [SerializeField] float radius = 0.15f; void Start() { material.SetTexture("_SubTex", texture); } void Update() { var mousePosition = Input.mousePosition; var uv = new Vector3( mousePosition.x / Screen.width, mousePosition.y / Screen.height, 0); material.SetVector("_Position", uv); material.SetFloat("_Aspect", Screen.height / (float) Screen.width); if (Input.GetMouseButtonDown(0)) { OpenPortal(); } else if (Input.GetMouseButtonUp(0)) { ClosePortal(); } } float currentPortalRadius = 0; void OpenPortal() { DOTween.KillAll(); DOTween.To(() => currentPortalRadius, SetPortalRadius, radius, 2f).SetEase(Ease.OutBack); } void ClosePortal() { DOTween.KillAll(); DOTween.To(() => currentPortalRadius, SetPortalRadius, 0f, 0.6f).SetEase(Ease.InBack); } void SetPortalRadius(float radius) { currentPortalRadius = radius; material.SetFloat("_Radius", radius); } void OnRenderImage(RenderTexture src, RenderTexture dest) { Graphics.Blit(src, dest, material); } }
補足
OnRenderImage を実装しているので、C#スクリプトはカメラにアタッチしてください。
アニメーションにDOTweenを使っているので、アセットストアからインポートが必要です。
https://www.assetstore.unity3d.com/jp/#!/content/27676
また、
material.SetFloat("_Radius", size);
上記のようにマテリアルにパラメータを渡している部分は、Shader.PropertyToID メソッドを使ってIDで参照した方がパフォーマンス的には良いです。今回は説明用にシンプルにするために使いませんでした。
readonly int radiusPropertyId = Shader.PropertyToID("_Radius"); void SetPortalRadius(float radius) { currentPortalRadius = radius; material.SetFloat(radiusPropertyId, radius); }
【Unity】ShaderLab でスケッチはじめました
はじめに
ShaderToy とか見てるとすごい(小並感)な作品がたくさんありますよね!
Shadertoy BETA
ここまではいかなくても、ちょっと自分の作品の演出でカッコいいこと出来るようになりたいので、いろんなテクニックについてじゃぶじゃぶインプット(&ぼちぼちアウトプット)しています。
ShaderLab のスケッチ置き場
GitHub - setchi/Unity-ShaderSketches: Self learning shader in Unity3D
GPGPUの習作置き場
GitHub - setchi/Unity-GPGPU-Sandbox: Self learning GPGPU in Unity3D
この記事では最近取り組んでいる ShaderLab でのスケッチについて紹介します。
Transform
まずは基礎からということで、基本的な座標変換を ShaderLab 上で実装してみました。
物体を移動・変形・回転させるには、行列を使うと便利です。行列とベクトルを掛けると色々な座標変換が実現できます。
行列とベクトルを掛けて座標変換していくイメージは、こちらの動画(2分あたりからの座標変換の説明)が分かり易かったです。クォータ二オンも学べます!!
【Unity道場 博多スペシャル 2017】クォータニオン完全マスター - YouTubewww.youtube.com
平行移動
float3x3 translate(float x, float y) { return float3x3(1, 0, x, 0, 1, y, 0, 0, 1); } float4 frag(v2f_img i) : SV_Target { float2 st = i.uv; float t = _Time.y; st = mul(translate(sin(t) * 0.35, cos(t) * 0.35), float3(st, 1)); return box(st, 0.25); }
Unity-ShaderSketches/Translate.shader at master · setchi/Unity-ShaderSketches · GitHub
translate 関数で変換行列を作り、座標系を移動させた後に箱を描画することで、動いているように見せています。
回転
float3x3 rotate(float angle) { return float3x3(cos(angle), -sin(angle), 0, sin(angle), cos(angle), 0, 0, 0, 1); } float4 frag(v2f_img i) : SV_Target { float2 st = i.uv; float t = _Time.y; st -= 0.5; st = mul(rotate(sin(t) * PI), float3(st, 1)); st += 0.5; return box(st, 0.4); }
Unity-ShaderSketches/Rotate.shader at master · setchi/Unity-ShaderSketches · GitHub
平行移動と同じく、rotate 関数で回転用の変換行列を作り、先に座標系を回転させてから箱を描画することで回転しているように見せています。
frag 関数内で st -= 0.5 と st += 0.5 しているのは、画面の真ん中を基準に回転させるためです。
拡大・縮小
float3x3 scale(float2 scale) { return float3x3(scale.x, 0, 0, 0, scale.y, 0, 0, 0, 1); } float4 frag(v2f_img i) : SV_Target { float2 st = i.uv; float t = _Time.y; st -= 0.5; st = mul(scale(sin(t) + 1), float3(st, 1)); st += 0.5; return box(st, 0.2); }
Unity-ShaderSketches/Scale.shader at master · setchi/Unity-ShaderSketches · GitHub
scale 関数で拡大・縮小用の変換行列を作り、座標系を拡大・縮小してから箱を描画することで動いているように見せています。
frag 関数内で st -= 0.5 と st += 0.5 しているのは、回転と同じく、画面の真ん中を基準に拡大・縮小させるためです。
Distance Field
次に学んだのは Distance Field を使った絵作りです。
Distance Field とは、ざっくり言うとある場所からの距離で空間を解釈し直す手法です。一部では文字の描画などにも使われています。
Distance Field に基づくノイズアルゴリズムに Cell Noise というものがあります。
Cell Noise
Cell Noise は、フィールド上の複数の点の中から最も近い点への距離を色で表したノイズです。
黒い部分の中心に点があると思ってください。その点に近いピクセルほど色が暗く、遠いピクセルほど色が明るくなっています。
Cell Noise を素直に ShaderLab で実装してアニメーションさせてみました。
Unity-ShaderSketches/CellNoise.shader at master · setchi/Unity-ShaderSketches · GitHub
Metaballs
Cell Noise をベースに違うエフェクトを作ってみました。
距離をどう解釈するかによって様々な模様を作れるのが Distance Field の面白いところのひとつだと思います。コードは Cell Noise とほぼ同じですが、step を使って点からの距離に対して段階的に色を出力しています。
Unity-ShaderSketches/Metaballs.shader at master · setchi/Unity-ShaderSketches · GitHub
Marble
こちらも Cell Noise のアレンジです。
動き的に Metaballs とほぼ同じですが、出力部分を smoothstep にして、その結果に対してさらに絞り込みをかけることで、点からの距離に対して輪切り状に色をつけています。
Unity-ShaderSketches/Marble.shader at master · setchi/Unity-ShaderSketches · GitHub
【Unity】モバイルで10万個のオブジェクトを描画してみた!
はじめに
大量のオブジェクトを描画するって魅力的じゃないですか!?
GPUの力を使ってやってみました!
初めてのGPGPUなので、今後のテンプレートとして使えるような出来る限りシンプルな構造のものを作ってみました。
今回はモバイルを含め、より多くの環境で実行できるようにコンピュートシェーダーは使わずに実装しました。
(絵心が足りなくて見た目が地味ですが、10万個のキューブが波打っています!)
この記事のプロジェクト一式はこちらです。
github.com
実装方針
主な登場人物です。
- レンダーテクスチャ(全オブジェクト位置を格納する箱として使用)
- フラグメントシェーダ(オブジェクト位置計算器)
- 制御用スクリプト(C#)
- キューブ表示用シェーダ(RenderTextureから自身の位置を取り出して、その位置に描画します)
10万ピクセルのレンダーテクスチャを用意して、各ピクセル値を各オブジェクトの位置情報として使います。位置情報はフラグメントシェーダを用いて一気に更新します!
実装
カーネル部分(フラグメントシェーダ)
uv値をもとに、パーリンノイズを使ってオブジェクト位置を出力しています。
Shader "UnityGpuSandbox/CubeWave/Kernels" { CGINCLUDE #include "UnityCG.cginc" sampler2D _PositionBuffer; float2 random2(float2 st) { st = float2(dot(st, float2(127.1, 311.7)), dot(st, float2(269.5, 183.3))); return -1.0 + 2.0 * frac(sin(st) * 43758.5453123); } float perlin_noise(float2 st) { float2 p = floor(st); float2 f = frac(st); float2 u = f * f * (3.0 - 2.0 * f); float v00 = random2(p + float2(0, 0)); float v10 = random2(p + float2(1, 0)); float v01 = random2(p + float2(0, 1)); float v11 = random2(p + float2(1, 1)); return lerp(lerp(dot(v00, f - float2(0, 0)), dot(v10, f - float2(1, 0) ), u.x), lerp(dot(v01, f - float2(0, 1)), dot(v11, f - float2(1, 1) ), u.x), u.y) + 0.5f; } float4 frag_init_position(v2f_img i) : SV_Target { i.uv -= 0.5; i.uv *= 150; return float4(i.uv.x, 0, i.uv.y, 1); } float4 frag_update_position(v2f_img i) : SV_Target { float4 p = tex2D(_PositionBuffer, i.uv); p.y = perlin_noise(float2(i.uv.x * 10, i.uv.y * 10 + _Time.x * 10)) * 10; return p; } ENDCG SubShader { Pass { CGPROGRAM #pragma target 3.0 #pragma vertex vert_img #pragma fragment frag_init_position ENDCG } Pass { CGPROGRAM #pragma target 3.0 #pragma vertex vert_img #pragma fragment frag_update_position ENDCG } } }
制御用スクリプト(C#)
public partial class CubeWave : MonoBehaviour { [SerializeField] Shader kernelShader; [SerializeField] Shader debugShader; [SerializeField] Mesh[] shapes = new Mesh[1]; [SerializeField] Material material; [SerializeField] ShadowCastingMode castShadows; [SerializeField] bool receiveShadows = false; [SerializeField] bool debug = false; RenderTexture positionBuffer1; RenderTexture positionBuffer2; Material kernelMaterial; Material debugMaterial; MaterialPropertyBlock props; BulkMesh bulkMesh; bool needsReset = true; RenderTexture CreateBuffer() { var width = bulkMesh.CopyCount; var height = 320; var buffer = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat); buffer.hideFlags = HideFlags.DontSave; buffer.filterMode = FilterMode.Point; buffer.wrapMode = TextureWrapMode.Repeat; return buffer; } Material CreateMaterial(Shader shader) { var material = new Material(shader); material.hideFlags = HideFlags.DontSave; return material; } void ResetResources() { if (bulkMesh == null) { bulkMesh = new BulkMesh(shapes, 320); } else { bulkMesh.Rebuild(shapes); } if (positionBuffer1) DestroyImmediate(positionBuffer1); if (positionBuffer2) DestroyImmediate(positionBuffer2); positionBuffer1 = CreateBuffer(); positionBuffer2 = CreateBuffer(); if (!kernelMaterial) kernelMaterial = CreateMaterial(kernelShader); if (!debugMaterial) debugMaterial = CreateMaterial(debugShader); InitializeBuffers(); needsReset = false; } void InitializeBuffers() { Graphics.Blit(null, positionBuffer2, kernelMaterial, 0); } void SwapBuffersAndInvokeKernels() { var tempPosition = positionBuffer1; positionBuffer1 = positionBuffer2; positionBuffer2 = tempPosition; kernelMaterial.SetTexture("_PositionBuffer", positionBuffer1); Graphics.Blit(null, positionBuffer2, kernelMaterial, 1); } void OnDestroy() { if (bulkMesh != null) bulkMesh.Release(); if (positionBuffer1) DestroyImmediate(positionBuffer1); if (positionBuffer2) DestroyImmediate(positionBuffer2); if (kernelMaterial) DestroyImmediate(kernelMaterial); if (debugMaterial) DestroyImmediate(debugMaterial); } void Update() { if (needsReset) { ResetResources(); } SwapBuffersAndInvokeKernels(); if (props == null) { props = new MaterialPropertyBlock(); } props.SetTexture("_PositionBuffer", positionBuffer2); var mesh = bulkMesh.Mesh; var pos = transform.position; var rot = transform.rotation; var mat = material; var uv = new Vector2(0.5f / positionBuffer2.width, 0); for (var i = 0; i < positionBuffer2.height; i++) { uv.y = (0.5f + i) / positionBuffer2.height; props.SetVector("_BufferOffset", uv); Graphics.DrawMesh( mesh, pos, rot, mat, 0, null, 0, props, castShadows, receiveShadows ); } } void OnGUI() { if (debug && Event.current.type.Equals(EventType.Repaint)) { if (debugMaterial && positionBuffer2) { var w = positionBuffer2.width; var h = positionBuffer2.height; var rect = new Rect(0, 0, w, h); Graphics.DrawTexture(rect, positionBuffer2, debugMaterial); } } } }
解説
制御用スクリプト(C#)の SwapBuffersAndInvokeKernels メソッド内でオブジェクト位置を計算&更新しています。
kernelMaterial.SetTexture("_PositionBuffer", positionBuffer1);
でマテリアルに現在のオブジェクト位置が入ったレンダーテクスチャをセットしています。
Graphics.Blit(null, positionBuffer2, kernelMaterial, 1);
でオブジェクト位置計算用マテリアルを使って positionBuffer2(レンダーテクスチャ) に新しいオブジェクト位置を描画(?)しています。
第四引数の「1」は、パスのインデックスの指定になっています。
シェーダのパス部分は下記のようになっており、上から順に0からインデックスが割り当てられています。「1」は二番目のパスになり、#pragma fragment で指定されている「frag_update_position」関数が呼び出されます。(「0」のパスはレンダーテクスチャ初期化用で、初期化時にC#スクリプトから呼んでいます)
SubShader { // pass 0 Pass { CGPROGRAM #pragma target 3.0 #pragma vertex vert_img #pragma fragment frag_init_position ENDCG } // pass 1 Pass { CGPROGRAM #pragma target 3.0 #pragma vertex vert_img #pragma fragment frag_update_position ENDCG } }
キューブの描画は、制御用スクリプト(C#)の Update 関数内が起点となっています。
MaterialPropertyBlock に位置情報を格納したレンダーテクスチャをセットして、Graphics.DrawMesh で描画しています。
if (props == null) { props = new MaterialPropertyBlock(); } props.SetTexture("_PositionBuffer", positionBuffer2); var mesh = bulkMesh.Mesh; var pos = transform.position; var rot = transform.rotation; var mat = material; var uv = new Vector2(0.5f / positionBuffer2.width, 0); for (var i = 0; i < positionBuffer2.height; i++) { uv.y = (0.5f + i) / positionBuffer2.height; props.SetVector("_BufferOffset", uv); Graphics.DrawMesh( mesh, pos, rot, mat, 0, null, 0, props, castShadows, receiveShadows ); }
実行結果
【Unity】線分同士の交差判定
実装時に少しはまってしまったので備忘録として残しておきます。
実装
(交差判定部分のみ)
public static bool LineSegmentsIntersection( Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, out Vector2 intersection) { intersection = Vector2.zero; var d = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x); if (d == 0.0f) { return false; } var u = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d; var v = ((p3.x - p1.x) * (p2.y - p1.y) - (p3.y - p1.y) * (p2.x - p1.x)) / d; if (u < 0.0f || u > 1.0f || v < 0.0f || v > 1.0f) { return false; } intersection.x = p1.x + u * (p2.x - p1.x); intersection.y = p1.y + u * (p2.y - p1.y); return true; }
動作
Gifのように動作するプロジェクト一式を公開しました。
github.com
【Unity】ベジェ曲線を学び、実装する
ベジェ曲線とは
数式で曲線を表現する方法の一つです。
コンピュータ上で滑らかな曲線を表現できるため、多くのドローソフトや文字の描画で採用されています。
ベジェ曲線の原理
ベジェ曲線を理解するには、1次ベジェ曲線から一つずつ次元を上げながら動きを見ていくのが分かりやすいと思います。
1次ベジェ曲線
*1
一本の線分(P0 から P1)の上を一定の割合で動く点を考えます。この点が動く軌跡が1次ベジェ曲線です。
つまり、ただの直線です。しかし、この動きがすべての基本になります。
「t」は線分上をどれだけの割合進んだのかを表す数値です。
2次ベジェ曲線
*2
2次ベジェ曲線は、線分が一本増えて二本の線分(P0 から P1 と、P1 から P2)から成り立ちます。
1次ベジェ曲線と同じように、それぞれの線分上を一定の割合で動く点(緑色の点)を考えます。
その点同士を結ぶと、なめらかに動く一本の線分(緑色の線分)が出来上がります。
さらに、緑色の線分上を一定の割合で動く点(黒い点)をまた考えることができます。この点が動く軌跡が2次ベジェ曲線です。
3次ベジェ曲線
*3
3次ベジェ曲線はさらに線分が一本増えて、三本の線分(P0 から P1、P1 から P2、P2 から P3)から成り立ちます。
1次ベジェ曲線と同じように、それぞれの線分上を一定の割合で動く点(緑色の点)を考えます。
その点同士を結ぶと、なめらかに動く二本の線分(緑色の線分)が出来上がります。
二本の緑色の線分に注目します。それぞれの線分上を一定の割合で動く点(青色の点)をまた考えることができます。
その点同士を結ぶと、なめらかに動く一本の線分(青色の線分)が出来上がります。
青色の線分に注目します。青色の線分上を一定の割合で動く点(黒い点)をまた考えることができます。この点が動く軌跡が3次ベジェ曲線です。
実装
今回は3次ベジェ曲線を実装しました。
UnityEngine.Vector3 には、ベクトルの線形補間を行う Lerp メソッドが用意されています。
Lerp メソッドを使って素直に実装すると下記のようになります。
Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { var a = Vector3.Lerp(p0, p1, t); // 緑色の点1 var b = Vector3.Lerp(p1, p2, t); // 緑色の点2 var c = Vector3.Lerp(p2, p3, t); // 緑色の点3 var d = Vector3.Lerp(a, b, t); // 青色の点1 var e = Vector3.Lerp(b, c, t); // 青色の点2 return Vector3.Lerp(d, e, t); // 黒色の点 }
これでも動きますが、数学的には下記のように最適化できます。
Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { var oneMinusT = 1f - t; return oneMinusT * oneMinusT * oneMinusT * p0 + 3f * oneMinusT * oneMinusT * t * p1 + 3f * oneMinusT * t * t * p2 + t * t * t * p3; }
この関数を使って、Illustratorのペンツール風(?)に曲線を描いていけるGUIを実装しました。
github.com
Unity で Illustrator のペンツール風にベジェ曲線が描けるやつ。コーナーポイント/スムーズポイントの切り替えとか、アンカーの削除ができる。ここから塗りつぶしとかいろいろ実装してみたくなる。#unity3d https://t.co/PjwLhtfttL pic.twitter.com/VkXPi3BUbS
— setchi (@setchi) April 9, 2017
まだ最低限の機能のみですが、機会を見つけたら実装練習も兼ねてリッチにしていきます。
参考
- 作者: 久富木隆一
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/09/24
- メディア: Kindle版
- この商品を含むブログを見る
【Unity】「FancyScrollView」を公開しました
高度に柔軟なアニメーションを実装できる汎用のScrollViewコンポーネントです。 無限スクロールも対応しています。
github.com
以下ほぼ README のコピペです
導入
Unity 2017.1.0 (C# 6.0) 以降が必要です。このリポジトリを Clone するか、 Asset Store からプロジェクトにインポートしてください。
サンプル
FancyScrollView/Examples/Scenes/ を参照してください。
サンプル名 | 説明 |
01_Basic | 最もシンプルな構成の実装例です。 |
02_CellEventHandling | セルからのイベントをハンドリングする実装例です。 |
03_InfiniteScroll | 無限スクロールの実装例です。 |
04_FocusOn | ボタンで左右のセルにフォーカスする実装例です。 |
仕組み
FancyScrollView はセルの位置を更新する際に、画面に見える範囲を正規化した値を各セルに渡します。セル側では 0.0 ~ 1.0 の値をもとにスクロール中の見た目を自由に制御できます。
使い方
もっともシンプルな構成では、
- セルにデータを渡すためのオブジェクト
- セル
- スクロールビュー
の実装が必要です。
スクリプトの実装
セルにデータを渡すためのオブジェクトを定義します。
public class ItemData { public string Message; }
FancyScrollViewCell を継承して自分のセルを実装します。
using UnityEngine; using UnityEngine.UI; using FancyScrollView; public class MyScrollViewCell : FancyScrollViewCell<ItemData> { [SerializeField] Text message; public override void UpdateContent(ItemData itemData) { message.text = itemData.Message; } public override void UpdatePosition(float position) { // position は 0.0 ~ 1.0 の値です // position に基づいてスクロールの外観を自由に制御できます } }
FancyScrollView を継承して自分のスクロールビューを実装します。
using UnityEngine; using System.Linq; using FancyScrollView; public class MyScrollView : FancyScrollView<ItemData> { [SerializeField] Scroller scroller; [SerializeField] GameObject cellPrefab; protected override GameObject CellPrefab => cellPrefab; void Start() { scroller.OnValueChanged(base.UpdatePosition); } public void UpdateData(IList<ItemData> items) { base.UpdateContents(items); scroller.SetTotalCount(items.Count); } }
スクロールビューにデータを流し込みます。
using UnityEngine; using System.Linq; public class EntryPoint : MonoBehaviour { [SerializeField] MyScrollView myScrollView; void Start() { var items = Enumerable.Range(0, 50) .Select(i => new ItemData {Message = $"Cell {i}"}) .ToArray(); myScrollView.UpdateData(items); } }
インスペクタ上の設定
My Scroll View
プロパティ | 説明 |
---|---|
Cell Spacing | セル同士の間隔を float.Epsilon ~ 1.0 の間で指定します。 |
Scroll Offset | スクロールのオフセットを指定します。例えば 0.5 を指定してスクロール位置が 0 の場合、最初のセルの位置が 0.5 になります。 |
Loop | オンにすると、セルをループして配置します。無限スクロールさせたい場合はオンにします。 |
Cell Prefab | セルの Prefab を指定します。 |
Cell Container | セルの親要素となる Transform を指定します。 |
Scroller
プロパティ | 説明 |
---|---|
Viewport | ビューポートとなる RectTransform を指定します。ここで指定された RectTransform の範囲内でジェスチャーの検出を行います。 |
Direction Of Recognize | ジェスチャーを認識する方向を Vertical か Horizontal で指定します。 |
Movement Type | コンテンツがスクロール範囲を越えて移動するときに使用する挙動を指定します。 |
Scroll Sensitivity | スクロールの感度を指定します。 |
Inertia | 慣性のオン/オフを指定します。 |
Deceleration Rate | Inertia がオンの場合のみ有効です。減速率を指定します。 |
Snap - Enable | Snap を有効にする場合オンにします。 |
Snap - Velocity Threshold | Snap がはじまる閾値となる速度を指定します。 |
Snap - Duration | Snap 時の移動時間を秒数で指定します。 |
Data Count | アイテムのデータ件数の総数です。基本的にスクリプトから設定します。 |
Q&A
データ件数が多くてもパフォーマンスは大丈夫?
表示に必要なセル数のみが生成されるため、データ件数がパフォーマンスに与える影響はわずかです。セル間のスペース(同時に存在するセルの数)とセルの演出は、データ件数よりもパフォーマンスに大きな影響を与えます。
自分でスクロール位置を制御したいんだけど?
FancyScrollView の次の API を使用してスクロール位置を更新できます。
protected void UpdatePosition(float position)
サンプルで使われている Scroller を使う場合は、次の API を使用して FancyScrollView のスクロール位置を更新できます。
public void ScrollTo(int index, float duration)
public void JumpTo(int index)
public void OnValueChanged(Action<float> callback)
Scroller を使わずにあなた自身の実装で全く違った振る舞いをさせることもできます。
セルで発生したイベントを受け取れる?
セル内で発生したあらゆるイベントをハンドリングできます。 サンプルにセルで発生したイベントをハンドリングする実装が含まれていますので、参考にして実装してください。
セルをLoop(無限スクロール)させたいんだけど?
無限スクロールをサポートしています。実装手順は下記の通りです。
- ScrollView の Loop をオンにするとセルが循環し、最初のセルの前に最後のセル、最後のセルの後に最初のセルが並ぶようになります。
- サンプルで使用されている Scroller を使う場合は、「Movement Type」を「Unrestricted」にするとスクロール範囲が無制限になります。
実装例( FancyScrollView/Assets/FancyScrollView/Examples/03_InfiniteScroll at master · setchi/FancyScrollView · GitHub
)が含まれていますので、こちらも参考にしてください。
その他、不具合・要望があれば
setchi (@setchi) | Twitter
に連絡いただくか、プルリクをお願いします。
【Unity】「HIT」のログイン時のようなエフェクトをつくる
HITとは?
NEXONが出してるスマホゲームです。UE4製で綺麗なグラフィックのアクションゲームです。
mobile.nexon.co.jp
ログイン時のエフェクトってどんなの?
実装方針
まずこんな画像を用意します。
こういうのはパーリンノイズが向いてる気がするので、パーリンノイズで生成しました。
色を高さと見なして、こんな風にしたらそれっぽいのができそうですね!
(図は Processing で生成しました)
EffectVisualize.pde · GitHub
実装
シェーダの全体像です。
Shader "Custom/LoginEffectShader" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {} _MaskTex("Mask Texture", 2D) = "white" {} _Height("Height", Float) = 0 } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" } Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO }; v2f vert(appdata_t IN) { v2f OUT; UNITY_SETUP_INSTANCE_ID(IN); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; return OUT; } sampler2D _MainTex; sampler2D _MaskTex; float _Height; fixed4 frag(v2f IN) : SV_Target { float maskHeight= tex2D(_MaskTex, IN.texcoord).r; if (maskHeight> _Height) { discard; } return tex2D(_MainTex, IN.texcoord); } ENDCG } } }
Properties ブロック内で、ノイズ画像の _MaskTex と現在の高さを表す _Height を定義します。
_MaskTex("Mask Texture", 2D) = "white" {} _Height("Height", Float) = 0
フラグメントシェーダ内で、今見ている位置より高い位置のピクセルを破棄しています。
fixed4 frag(v2f IN) : SV_Target { float maskHeight = tex2D(_MaskTex, IN.texcoord).r; if (maskHeight > _Height) { discard; } return tex2D(_MainTex, IN.texcoord); }
あとは、C# スクリプトから適当に _Height を操作します。
using DG.Tweening; using UnityEngine; public class EffectTweener : MonoBehaviour { [SerializeField] SpriteRenderer _spriteRenderer; Material _material; int _heightPropertyID; bool _isHidden = false; void Start() { _heightPropertyID = Shader.PropertyToID("_Height"); _material = _spriteRenderer.material; } void Update() { if (Input.GetMouseButtonDown(0)) { if (_isHidden) { Play(from: 0f, to: 1f); } else { Play(from: 1f, to: 0f); } _isHidden = !_isHidden; } } void Play(float from, float to) { DOTween.Kill(this); DOTween.To( () => from, a => _material.SetFloat(_heightPropertyID, a), to, 1.5f) .SetId(this); } }
ここまでの結果です。
仕上げ
フチに色を付けたり、Bloomで光らせたりして盛ります。
fixed4 frag(v2f IN) : SV_Target { float maskHeight = tex2D(_MaskTex, IN.texcoord).r; if (maskHeight > _Height) { discard; } fixed4 color = tex2D(_MainTex, IN.texcoord); float edgeHeight = 0.015; return lerp( color, fixed4(0, 4, 2, 0), step(_Height - edgeHeight, maskHeight) ); }
まとめ
マスク画像を工夫すればもっと面白い表現ができそうです。
できればフラグメントシェーダ内の条件分岐をなくしたいです。
2017/01/26追記:シェーダーの改善
今回の記事のような処理は、αカットアウトシェーダを使うとよりシンプルに実装できると指摘をいただきました。
docs.unity3d.com
Unityのビルトインシェーダにあるαカットアウトシェーダ(Unlit/Transparent Cutout)を使って実装してみます。
仕組み
αカットアウトシェーダは名前の通り、画像のα値と閾値によってピクセルを破棄するか決定します。
元画像のα成分にマスク情報を埋め込んだ画像を用意します。
この画像を、Unlit/Transparent Cutout シェーダをセットしたマテリアルで描画して、_Cutoff プロパティを操作するとこのように動きます。
(前回と背景が変わっていますが、分かりやすさのためです。結果は前回とほぼ同じです)
フチに色を付ける
Unityのビルトインシェーダはここからダウンロードできます。
https://unity3d.com/jp/get-unity/download/archive
フチに色が付くように Unlit/Transparent Cutout の frag 関数内を改造したシェーダをつくりました。
fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.texcoord); clip(col.a - _Cutoff); UNITY_APPLY_FOG(i.fogCoord, col); float edgeHeight = 0.015; return lerp( col, fixed4(0, 4, 2, 1), step(col.a, _Cutoff + edgeHeight) ); }
改造したシェーダの全体像はこちらです。
https://gist.github.com/setchi/b5c9fd72c3cb5317dae44cb6f3eb7fef
実行結果です。
最初の実装と比較して
最初の実装では画像を二枚使用していたのに対し、一枚の画像で実装できました。
また、不要なピクセルをより手前(Alpha Test)の段階で破棄できて効率的になりました。
【Unity】uGUIで使えるハイパーテキストを作りました
経緯
以前 uGUI Text で Twitter のクリック可能なハッシュタグのようなものを実装しようとして挫折したのをふと思い出して再挑戦しました。
作ったもの
uGUI Text の指定した部分文字列にクリック時のコールバックや文字色を設定できる仕組みを作りました。
github.com
使い方
Text を継承した抽象クラスの HypertextBase.cs に、任意の位置の文字列に文字色とクリックされたときのコールバックを設定できるAPI を用意しているので、継承して自分の好きなように実装します。
サンプルとして正規表現によるハイパーテキストの実装例を置いてあります。
RegexHypertext.cs
uGUI-Hypertext/RegexHypertext.cs at master · setchi/uGUI-Hypertext · GitHub
こんな風に使えます。
using UnityEngine; public class RegexExample : MonoBehaviour { [SerializeField] RegexHypertext text; const string RegexURL = @"https?://(?:[!-~]+\.)+[!-~]+"; const string RegexHashtag = @"[##][A-Za-zA-Za-z一-鿆0-90-9ぁ-ヶヲ-゚ー]+"; void Start() { text.OnClick(RegexURL, Color.cyan, url => Debug.Log(url)); text.OnClick(RegexHashtag, Color.green, hashtag => Debug.Log(hashtag)); } }
結果
おわりに
現状、Canvas の Render Mode が Screen Space - Overlay の場合にしか対応できていません。今後対応予定です。
2016/08/22 追記: すべての Render Mode に対応しました。
その他、不具合・要望があれば
setchi (@setchi) | Twitter
に連絡ください!
【Unity】シェーダーを利用して音声波形を描く
今、広い範囲の音声波形を高速にリアルタイム描画する問題に取り組んでいます。
要件として描画対象の範囲をグリグリ変更できる必要があって、これまでLineRendererやGLによる描画を試みましたがどれも欲しいパフォーマンスに届きませんでした。
そこで、波形情報をテクスチャに埋め込んでシェーダーで描画する方法を試してみました。
AudioClipから波形情報を取得する
var audioSource = GetComponent<AudioSource>(); var samples = new float[audioSource.clip.samples * audioSource.clip.channels]; audioSource.clip.GetData(samples, 0);
で取得できます。
docs.unity3d.com
波形情報をテクスチャに埋め込む
描画領域の横幅 x 1サイズのテクスチャを生成して、色情報に波形データを埋め込んでいきます。
手抜き実装なので r 成分しか使ってませんが、rgba 全ての成分を使えばより小さいテクスチャサイズで実現できます。
また、1サンプルを1ピクセルに対応させるとテクスチャサイズが膨大になりすぎるので適当に間引きします。
public class WaveformRenderer : MonoBehaviour { [SerializeField] AudioSource audioSource; [SerializeField] RawImage image; [SerializeField] int imageWidth; Texture2D texture; float[] samples = new float[500000]; void Start() { texture = new Texture2D(imageWidth, 1); texture.SetPixels(Enumerable.Range(0, imageWidth).Select(_ => Color.clear).ToArray()); texture.Apply(); image.texture = texture; } void Update() { audioSource.clip.GetData(samples, audioSource.timeSamples); int textureX = 0; int skipSamples = 200; float maxSample = 0; for (int i = 0, l = samples.Length; i < l && textureX < imageWidth; i++) { maxSample = Mathf.Max(maxSample, samples[i]); if (i % skipSamples == 0) { texture.SetPixel(textureX, 0, new Color(maxSample, 0, 0)); maxSample = 0; textureX++; } } texture.Apply(); } }
説明に余分なコードは省いています。実際のソースコードはここにあります。
NoteEditor/WaveformRenderer.cs at 21a1878556f1b492a7a9a150339512a6ba5330ca · setchi/NoteEditor · GitHub
このようなテクスチャが生成されます。
(実際は縦1pxです。見やすく縦方向に伸ばしています)
シェーダーで波形を描画する
フルソースはここにあります。
NoteEditor/Waveform.shader at a56ec2d55987a8f22c392bfe1739af87a9e97bc9 · setchi/NoteEditor · GitHub
重要なのは下記の部分です。
テクスチャの r 成分からボリュームを取り出して、自身のV座標がボリュームの範囲内なら緑、そうでなければ黒を出力しています。
fixed4 frag(v2f v) : SV_Target{ float volume = tex2D(_MainTex, v.uv.x).r * 0.5; float uvY = v.uv.y - 0.5; return lerp( fixed4(0, 0, 0, 1), fixed4(0, 1, 0, 1), -volume < uvY && uvY < volume ); }
最終的にこのように出力されます。