概要
前記事ではUnityでステンシルボリュームシャドウを描画する方法について書きました。
daiki-eguchi.hatenablog.com
昨今のGPUではテクセルフィルレートが劇的に向上したことで昨今主流となったのがデプスシャドウ技法のようです。
参考:
3Dグラフィックス・マニアックス(24) 影の生成(5)~デプスシャドウ技法 | マイナビニュース
テクセルフィルレートというのはテクスチャを構成する単位のテクセルの処理速度をいい、処理速度が昨今早くなったことから主流になった手法ということですね。
デプスシャドウ技法とは
光源を仮想の視点と捉えて光源から光が当たっているかどうかをシャドウマップに書き込む。
その後、シャドウマップを投影テクスチャマッピングの用法でオブジェクトに貼り付けるイメージで描画することで影が描画される仕組みです。
ではどのようにして「光が当たっているかどうか」を書き込むかです。
カメラから見て、レンダリングしようとしているピクセルと光源までの距離をdとして、光源からそのピクセルまで飛ばしたRayが衝突したところまでの距離をsとします。
以下のように2点考えると、s=dとなるところはレンダリングしようとしているピクセルと光源の間に遮蔽物がなかったことになるので「影ではない」ということになります。
d>sとなるところではレンダリングしようとしているピクセルと光源の間に遮蔽物があったことになるので「影」ということになります。
Unityでデプスシャドウ技法を行う
Unityでデプスシャドウ技法を行うとき、シャドウマップの描画はUnityがよしなにやってくれます。ただ今回はせっかくなのでSRPからシャドウマップの描画自体も行ってみましょう。
SRPからデプスシャドウ技法を行う
SRPからデプスシャドウ技法を行っていこうと思いますが、素晴らしい記事があるのでこちらを参照してください。
この記事を実装するとDirectionalLight1つのシャドウを描画することができます。
今回は、僭越ながらこの記事を実装した前提で複数のDirectionalLightの影を描くように実装してみます。
qiita.com
qiita.com
成果物
まずは今回の成果物です。DirectionalLightを4つおいて様々な方向を向かせていますがどのライトからのシャドウも描画されています。
シャドウマップの描画
では複数のDirectionalLightの影を落とすときはどのようにしたらよいのでしょうか。
今回、シャドウマップは以下のように描画されています。(シャドウオンのライトが4つの時)
これは、大きなシャドウマップに対して以下のようにそれぞれのライトのシャドウを分割して描画されています。
今回、影を落とせるライトの最大数を4つとしたので4つにシャドウマップを分けています。しかし、最大数が4だとしてもシーンにDirectionalLightが1つのときにこの分割数だとシャドウの品質が下がってしまいます。デプスシャドウによるシャドウの品質はシャドウマップの解像度と直結するからです。
そのため、DirectionalLightが1つの時は以下のように分けずにシャドウを描画します。
実装
コード全文は長いので折りたたみました。大事なところは抽出して説明いたします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class RenderPipeline : UnityEngine.Rendering.RenderPipeline
{
private const string PipelineName = "ShadowRenderPipeline";
private const int MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT = 4;
private readonly RenderPipelineAsset Asset;
private readonly int RenderTarget;
private readonly int LightShadow;
private readonly RenderTargetIdentifier LightShadowId;
private readonly int LightVP;
private readonly int LightDirs;
private readonly int LightColors;
private readonly int ShadowBiases;
private readonly int ShadowNormalBiases;
private readonly int ShadowDistanceSqrts;
private readonly int DirShadowMatricesId;
private readonly int DirectionalLightCountId;
private readonly int OffsetsId;
private readonly Matrix4x4 [] DirShadowMatrices = new Matrix4x4[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly Vector4[] DirectionalLightsDirections = new Vector4[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly Vector4[] DirectionalLightsColors = new Vector4[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly Vector4[] Offsets = new Vector4[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowBiases = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowNormalBiases = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowDistanceSqrts = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly RenderTargetIdentifier RenderTargetId;
private readonly RenderTargetIdentifier CameraTargetId;
private readonly ShaderTagId RenderTagId;
public RenderPipeline(RenderPipelineAsset asset)
{
Asset = asset;
RenderTarget = Shader.PropertyToID("_RenderTarget");
RenderTargetId = new RenderTargetIdentifier(RenderTarget);
CameraTargetId = new RenderTargetIdentifier(BuiltinRenderTextureType.CameraTarget);
RenderTagId = new ShaderTagId("Forward");
LightShadow = Shader.PropertyToID("_LightShadow");
LightShadowId = new RenderTargetIdentifier(LightShadow);
LightVP = Shader.PropertyToID("_LightVP");
LightDirs = Shader.PropertyToID("_LightDirs");
LightColors = Shader.PropertyToID("_LightColors");
ShadowBiases = Shader.PropertyToID("_ShadowBiases");
ShadowNormalBiases = Shader.PropertyToID("_ShadowNormalBiases");
ShadowDistanceSqrts = Shader.PropertyToID("_ShadowDistanceSqrts");
DirShadowMatricesId = Shader.PropertyToID("_DirectionalShadowMatrices");
DirectionalLightCountId = Shader.PropertyToID("_DirectionalLightCount");
OffsetsId = Shader.PropertyToID("_Offsets");
}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
var shadowResolution = Asset.ShadowResolution;
var shadowDistance = Asset.ShadowDistance;
foreach (var camera in cameras)
{
var cmd = CommandBufferPool.Get(PipelineName);
context.SetupCameraProperties(camera);
if (!camera.TryGetCullingParameters(false, out var cullingParameters))
{
continue;
}
cullingParameters.shadowDistance = Mathf.Clamp(shadowDistance, camera.nearClipPlane, camera.farClipPlane);
var cullingResults = context.Cull(ref cullingParameters);
var lightIndexes = SearchLightIndexes(cullingResults, LightType.Directional);
int splitCount = lightIndexes.Count <= 1 ? 1 : 2;
float tileSize = shadowResolution / splitCount;
var existValidLight = lightIndexes != null && lightIndexes.Count > 0;
if (existValidLight)
{
SetupLightRT(context, cmd, shadowResolution, lightIndexes.Count);
for (int i = 0; i < lightIndexes.Count; i++)
{
var lightIndex = lightIndexes[i];
var light = cullingResults.visibleLights[lightIndex];
var lightVP = CalcLightViewProjection(cullingResults, lightIndex);
SetupLightVP(context, cmd, lightVP);
DrawShadow(context, cmd, cullingResults, lightIndex, i, splitCount, tileSize, light.light, shadowResolution, shadowDistance);
}
SetupLightParameters(context, cmd);
}
SetupMainRT(context, cmd, camera);
DrawOpaque(context, cmd, camera, cullingResults);
context.DrawSkybox(camera);
cmd.Clear();
cmd.Blit(RenderTargetId, CameraTargetId);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
cmd.ReleaseTemporaryRT(RenderTarget);
context.ExecuteCommandBuffer(cmd);
if (existValidLight)
{
CleanupLightRT(context, cmd);
}
CommandBufferPool.Release(cmd);
}
context.Submit();
}
private List<int> SearchLightIndexes(CullingResults cullingResults, LightType lightType)
{
var lights = new List<int>();
for (int i = 0; i < cullingResults.visibleLights.Length; i++)
{
var visibleLight = cullingResults.visibleLights[i];
if (visibleLight.lightType != lightType)
{
continue;
}
var light = visibleLight.light;
if(light == null || light.shadows == LightShadows.None || light.shadowStrength <= 0)
{
continue;
}
if(!cullingResults.GetShadowCasterBounds(i, out var bounds))
{
continue;
}
lights.Add(i);
}
return lights;
}
private void SetupMainRT(ScriptableRenderContext context, CommandBuffer cmd, Camera camera)
{
cmd.Clear();
cmd.GetTemporaryRT(RenderTarget, Screen.width, Screen.height, 32);
cmd.SetRenderTarget(RenderTarget);
cmd.ClearRenderTarget(true, false, camera.backgroundColor, 1);
context.ExecuteCommandBuffer(cmd);
}
private void DrawOpaque(ScriptableRenderContext context, CommandBuffer cmd, Camera camera, CullingResults cullingResults)
{
cmd.Clear();
cmd.SetRenderTarget(RenderTargetId);
context.ExecuteCommandBuffer(cmd);
var opaqueSortingSettings = new SortingSettings(camera) {criteria = SortingCriteria.CommonOpaque};
var opaqueDrawSettings = new DrawingSettings(RenderTagId, opaqueSortingSettings);
var opaqueRenderQueueRange = new RenderQueueRange(0, (int) RenderQueue.GeometryLast);
var opaqueFilterSettings = new FilteringSettings(opaqueRenderQueueRange, camera.cullingMask);
context.DrawRenderers(cullingResults, ref opaqueDrawSettings, ref opaqueFilterSettings);
}
private Matrix4x4 CalcLightViewProjection(CullingResults cullingResults, int lightIndex)
{
var light = cullingResults.visibleLights[lightIndex].light;
cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(
lightIndex,
0,
1,
Vector3.zero,
0,
light.shadowNearPlane,
out var viewMatrix,
out var projMatrix,
out var shadowSplitData);
projMatrix = GL.GetGPUProjectionMatrix(projMatrix, true);
return projMatrix * viewMatrix;
}
private void SetupLightVP(ScriptableRenderContext context, CommandBuffer cmd, Matrix4x4 lightVP)
{
cmd.Clear();
cmd.SetGlobalMatrix(LightVP, lightVP);
context.ExecuteCommandBuffer(cmd);
}
private void SetupLightParameters(ScriptableRenderContext context, CommandBuffer cmd)
{
cmd.Clear();
cmd.SetGlobalMatrixArray(DirShadowMatricesId, DirShadowMatrices);
cmd.SetGlobalVectorArray(LightDirs, DirectionalLightsDirections);
cmd.SetGlobalVectorArray(LightColors, DirectionalLightsColors);
cmd.SetGlobalVectorArray(OffsetsId, Offsets);
cmd.SetGlobalFloatArray(ShadowBiases, LightShadowBiases);
cmd.SetGlobalFloatArray(ShadowNormalBiases, LightShadowNormalBiases);
cmd.SetGlobalFloatArray(ShadowDistanceSqrts, LightShadowDistanceSqrts);
context.ExecuteCommandBuffer(cmd);
}
private Vector2 SetTileViewport(CommandBuffer cmd, int index, int split, float tileSize, int shadowResolution)
{
Vector2 offset = new Vector2(index % split, index / split);
Vector2 tileOffset = offset * tileSize;
Offsets[index] = tileOffset / shadowResolution;
cmd.SetViewport(new Rect(offset.x * tileSize, offset.y * tileSize, tileSize, tileSize));
return offset;
}
private void SetupLightRT(ScriptableRenderContext context, CommandBuffer cmd, int shadowResolution, int directionalLightCount)
{
cmd.Clear();
cmd.GetTemporaryRT(LightShadow, shadowResolution, shadowResolution, 32, FilterMode.Bilinear,
RenderTextureFormat.RFloat);
cmd.SetRenderTarget(LightShadowId);
cmd.SetGlobalInt(DirectionalLightCountId, directionalLightCount);
cmd.ClearRenderTarget(true, true, Color.white, 1);
context.ExecuteCommandBuffer(cmd);
}
private void DrawShadow(ScriptableRenderContext context, CommandBuffer cmd, CullingResults cullingResults,
int lightIndex, int index, int split, float tileSize, Light light, int shadowResolution, float shadowDistance)
{
cmd.Clear();
cmd.SetRenderTarget(LightShadowId);
SetTileViewport(cmd, index, split, tileSize, shadowResolution);
DirShadowMatrices[index] = CalcLightViewProjection(cullingResults, lightIndex);
DirectionalLightsDirections[index] = -light.transform.forward;
DirectionalLightsColors[index] = light.color * light.intensity;
LightShadowBiases[index] = light.shadowBias;
LightShadowNormalBiases[index] = light.shadowNormalBias;
LightShadowDistanceSqrts[index] = shadowDistance * shadowDistance;
context.ExecuteCommandBuffer(cmd);
var shadowDrawingSettings = new ShadowDrawingSettings(cullingResults, lightIndex);
context.DrawShadows(ref shadowDrawingSettings);
cmd.Clear();
cmd.SetGlobalTexture(LightShadow, LightShadowId);
context.ExecuteCommandBuffer(cmd);
}
private void CleanupLightRT(ScriptableRenderContext context, CommandBuffer cmd)
{
cmd.Clear();
cmd.ReleaseTemporaryRT(RenderTarget);
context.ExecuteCommandBuffer(cmd);
}
}
Shader "Unlit/Sample"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags
{
"Queue" = "Geometry"
"RenderType" = "Opaque"
}
UsePass "Hidden/ShadowCaster/SHADOW_CASTER"
Pass
{
Tags {"LightMode" = "Forward"}
LOD 100
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Light.hlsl"
struct appdata
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 positionWS : TEXCOORD0;
float2 uv : TEXCOORD1;
float3 normalWS : NORMAL;
float4 positionCS : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
v2f vert (appdata v)
{
v2f o;
o.positionCS = UnityObjectToClipPos(v.positionOS);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.positionWS = mul(unity_ObjectToWorld, v.positionOS);
o.normalWS = UnityObjectToWorldNormal(v.normal);
return o;
}
half4 frag (v2f i) : SV_Target
{
half3 color = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
for (int t = 0; t < _DirectionalLightCount; t++)
{
float dotNL = max(0, dot(normalize(i.normalWS), _LightDirs[t]));
half3 diffuse = _LightColors[t] * dotNL;
float shadowAttenuation = GetShadowAttenuation(i.positionWS, dotNL, t, _DirectionalLightCount);
color *= diffuse * shadowAttenuation;
}
return half4(color, 1);
}
ENDCG
}
}
}
#ifndef LIGHT
#define LIGHT
#include "UnityCG.cginc"
#endif
struct DirectionalLight
{
float3 lightDir;
half3 lightColor;
};
#define MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT 4
float4x4 _LightVP;
float4x4 _DirectionalShadowMatrices[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
float3 _LightDirs[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
half3 _LightColors[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
float _ShadowBiases[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
float _ShadowNormalBiases[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
float _ShadowDistanceSqrts[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
float2 _Offsets[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
sampler2D _LightShadow;
int _DirectionalLightCount;
float4 TransformWorldToLightViewProjection(float3 positionWS, int tileIndex)
{
float4 positionCS = mul(_DirectionalShadowMatrices[tileIndex], float4(positionWS, 1.0));
return positionCS;
}
float4 TransformObjectToLightViewProjection(float3 positionOS)
{
return mul(_LightVP, mul(unity_ObjectToWorld, float4(positionOS, 1)));
}
float CalcLightViewProjectionDepth(float4 positionLVP)
{
float depth = positionLVP.z / positionLVP.w;
#if UNITY_REVERSED_Z
return 1-depth;
#else
return depth;
#endif
}
float GetShadowAttenuation(float4 positionWS, float dotNL, int tileIndex, int split)
{
float4 positionLVP = TransformWorldToLightViewProjection(positionWS, tileIndex);
float2 samplingUv = 0;
float2 uv = positionLVP.xy / positionLVP.w * float2(0.5f, -0.5f) + 0.5f;
float scale = split == 1 ? 1 : 0.5;
samplingUv = Offsets[tileIndex] + uv * scale;
float3 positionVS = mul(unity_MatrixV, positionWS);
float distanceSqrt = dot(positionVS.xyz, positionVS.xyz);
float zInLVP = CalcLightViewProjectionDepth(positionLVP);
float zInShadow = tex2D(_LightShadow, samplingUv.xy).r;
float bias = _ShadowNormalBiases[tileIndex] * tan(acos(dotNL)) + _ShadowBiases[tileIndex];
float attenuation = zInLVP - bias > zInShadow ? 0 : 1;
return samplingUv.x > 0 && samplingUv.x < 1 && samplingUv.y > 0 && samplingUv.y < 1 && distanceSqrt <= _ShadowDistanceSqrts[tileIndex] ? attenuation : 1;
}
複数のDirectionalLightの影を描画するため、C#側は大きく二つのことを行います。ひとつは上記でお話ししたシャドウマップ描画のタイリング。そしてもう一つはライトのパラメーターを複数シェーダーに渡すために行う配列化です。
シャドウマップのタイリング
上記で説明したようにライトが1つだったときと2~4つだった時でシャドウのタイリングが変わります。Render関数内でその判定を行っています。
ライトの個数が1つの時はRenderPipelineAsset側で指定したシャドウ解像度が分割後のタイルサイズと一致するように。つまりシャドウマップ用のバッファ全体に影を描画できるようにします。
ライトの個数が2つ以上の時はRenderPipelineAsset側で指定したシャドウ解像度を2分割し、タイルのサイズとします。これによって、一枚のシャドウマップ用バッファに4つのライトシャドウ情報を入れることができます。
var lightIndexes = SearchLightIndexes(cullingResults, LightType.Directional);
int splitCount = lightIndexes.Count <= 1 ? 1 : 2;
float tileSize = shadowResolution / splitCount;
シャドウマップへの描画はDrawShadowで行っています。上記で説明したシャドウマップの分割を以下のSetTileViewport(cmd, index, split, tileSize, shadowResolution);
で行っています。
private void DrawShadow(ScriptableRenderContext context, CommandBuffer cmd, CullingResults cullingResults,
int lightIndex, int index, int split, float tileSize, Light light, int shadowResolution, float shadowDistance)
{
cmd.Clear();
cmd.SetRenderTarget(LightShadowId);
SetTileViewport(cmd, index, split, tileSize, shadowResolution);
}
SetTileViewportの中身は大事なTile分割です。
各ライトのシャドウに対してoffsetと名付けたVector2をフィールド変数に保存しています。各ライトがシャドウマップのどこに描画しているかを表すものです。これが必要なのはShader側も、どのライトのシャドウ情報が一枚のシャドウマップのどこからどこまでにあるのかを知る必要があるためです。
実際にタイリングしているのはcmd.SetViewport(new Rect(offset.x * tileSize, offset.y * tileSize, tileSize, tileSize));
の部分です。
private Vector2 SetTileViewport(CommandBuffer cmd, int index, int split, float tileSize, int shadowResolution)
{
Vector2 offset = new Vector2(index % split, index / split);
Vector2 tileOffset = offset * tileSize;
Offsets[index] = tileOffset / shadowResolution;
cmd.SetViewport(new Rect(offset.x * tileSize, offset.y * tileSize, tileSize, tileSize));
return offset;
}
ライト情報の配列化
参考の記事でfloat定義であったLightのパラメーターを配列に変更します。複数のライトがあり、それぞれにBias等の設定があるのでそれを反映するためになります。
private readonly float[] LightShadowBiases = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowNormalBiases = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowDistanceSqrts = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
そして、ループを回してすべてのライトのShadowMapを描いた後にパラメーターをShader側に渡すように変更しています。ライトの情報をすべて配列に格納し終えてからShaderに渡すためです。
if (existValidLight)
{
SetupLightRT(context, cmd, shadowResolution, lightIndexes.Count);
for (int i = 0; i < lightIndexes.Count; i++)
{
var lightIndex = lightIndexes[i];
var light = cullingResults.visibleLights[lightIndex];
var lightVP = CalcLightViewProjection(cullingResults, lightIndex);
SetupLightVP(context, cmd, lightVP);
DrawShadow(context, cmd, cullingResults, lightIndex, i, splitCount, tileSize, light.light, shadowResolution, shadowDistance);
}
SetupLightParameters(context, cmd);
}
オブジェクトシェーダー側
オブジェクトにアタッチするシェーダー側の変更点もC#側の変更に合わせて大きく二つです。
一つ目はDirectionalLightが複数あるのでフラグメントシェーダーで複数のライトの処理を行えるようにループ処理させることです。以下はフラグメントシェーダー内でループ処理している部分の抽出です。
for (int t = 0; t < _DirectionalLightCount; t++)
{
float dotNL = max(0, dot(normalize(i.normalWS), _LightDirs[t]));
half3 diffuse = _LightColors[t] * dotNL;
float shadowAttenuation = GetShadowAttenuation(i.positionWS, dotNL, t, _DirectionalLightCount);
color *= diffuse * shadowAttenuation;
}
そしてもう一つはシャドウマップのサンプリング位置の調整です。以下のコードはシャドウを求めているところですが、参考の記事と比べてライトのパラメーターが配列化しているのでその変更部分。そして各ライトのシャドウがシャドウマップ内のどこにあるのかをC#側からもらっているのでサンプリング位置の調整が入っています。
float GetShadowAttenuation(float4 positionWS, float dotNL, int tileIndex, int split)
{
float4 positionLVP = TransformWorldToLightViewProjection(positionWS, tileIndex);
float2 samplingUv = 0;
float2 uv = positionLVP.xy / positionLVP.w * float2(0.5f, -0.5f) + 0.5f;
float scale = split == 1 ? 1 : 0.5;
samplingUv = Offsets[tileIndex] + uv * scale;
float3 positionVS = mul(unity_MatrixV, positionWS);
float distanceSqrt = dot(positionVS.xyz, positionVS.xyz);
float zInLVP = CalcLightViewProjectionDepth(positionLVP);
float zInShadow = tex2D(_LightShadow, samplingUv.xy).r;
float bias = _ShadowNormalBiases[tileIndex] * tan(acos(dotNL)) + _ShadowBiases[tileIndex];
float attenuation = zInLVP - bias > zInShadow ? 0 : 1;
return samplingUv.x > 0 && samplingUv.x < 1 && samplingUv.y > 0 && samplingUv.y < 1 && distanceSqrt <= _ShadowDistanceSqrts[tileIndex] ? attenuation : 1;
}
サンプリング位置の調整は以下の部分です。uvをだしたところは投影テクスチャマッピングと同じように算出していますが、その値にscaleをかけることとオフセットをつけることでサンプリング位置を調整しています。
float2 samplingUv = 0;
float2 uv = positionLVP.xy / positionLVP.w * float2(0.5f, -0.5f) + 0.5f;
float scale = split == 1 ? 1 : 0.5;
samplingUv = Offsets[tileIndex] + uv * scale;
最後に
上記によって以下のように複数のDirectionalLightのシャドウを描くことができました。
1つのDirectionalLightのシャドウを描くときも同様ですが、うまくシャドウが描画できないときは各ライトコンポーネントのBiasやStrength等を調整してみてください。
参考
catlikecoding.com
news.mynavi.jp
qiita.com
qiita.com
鈴木直美の「PC Watch先週のキーワード」