效果还原-少女前线2:追放 vepley角色渲染分析

一、贴图分析

BR流程,贴图mask只有PBR的roughness、metallic、occlusion,不像战双那种PBRMask和ILM贴图全上。模型和法线做的很细,拆成两张图来存保证精度。

衣物

效果还原-少女前线2:追放 vepley角色渲染分析
d
效果还原-少女前线2:追放 vepley角色渲染分析
rmo
效果还原-少女前线2:追放 vepley角色渲染分析
normal
效果还原-少女前线2:追放 vepley角色渲染分析
envCube
效果还原-少女前线2:追放 vepley角色渲染分析
RampTex
RampTex:
第一行:additional light shadowRamp
第二行:metal env blinnphong specularRamp (NdotH to ramp? undetermind)
第三行:direct/additional light specularRamp
第四行:direct light shadowRamp

注意到directlight shadowRamp的最左边不为0,可能是想往暗部加一种散射的感觉把,pbr模型的直接光阴影部分是全黑的,加一个基础色。额外光源就是一个很软的过渡。

specularRamp最左边都为0,高光只影响到高光区域。


皮肤

贴图和上面衣物一样,
效果还原-少女前线2:追放 vepley角色渲染分析
skin RampTex
能看到皮肤的shadowRamp的sss效果很明显


头发

效果还原-少女前线2:追放 vepley角色渲染分析
hair_d
效果还原-少女前线2:追放 vepley角色渲染分析
hairSpec
效果还原-少女前线2:追放 vepley角色渲染分析
envCube
效果还原-少女前线2:追放 vepley角色渲染分析
hairRampTex
没有做法线,模型本身是映射的球形法线。 仅仅一张高光mask,diffuse图比较明显得加上了AO信息。金属度粗糙度就到材质球直接整体调整了。


面部

效果还原-少女前线2:追放 vepley角色渲染分析
face_d
效果还原-少女前线2:追放 vepley角色渲染分析
face sdf shadow
效果还原-少女前线2:追放 vepley角色渲染分析
envCube
效果还原-少女前线2:追放 vepley角色渲染分析
faceRampTex
很经典的SDF面部阴影图,很精细,但它多了个ba通道,这个之后说。


眼睛

效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
眼睛、高光、阴影单独做,这样有一个好处就是阴影和高光都能根据视角发生变化,给人的感觉就是正确的眼球表面高光,正确的眉投射下的阴影。


绒绒毛

效果还原-少女前线2:追放 vepley角色渲染分析
heightNoise
效果还原-少女前线2:追放 vepley角色渲染分析
d
效果还原-少女前线2:追放 vepley角色渲染分析
normal
效果还原-少女前线2:追放 vepley角色渲染分析
envCube
效果还原-少女前线2:追放 vepley角色渲染分析
Plush RampTex
heightNoise,多Pass毛的噪声。d和n是对应的,用sd很好做出来。但是,这里多Pass出来的AO没有加,加上也很难看,只是表现个凸出边边。
ramp图的漫反射是青光的,高光是蓝白的,我有点在意为什么有个青色而不是淡蓝。


二、模型分析

衣物、皮肤
效果还原-少女前线2:追放 vepley角色渲染分析
mesh info
效果还原-少女前线2:追放 vepley角色渲染分析
color.rgb
效果还原-少女前线2:追放 vepley角色渲染分析
color.a
材质以双面/单面渲染拆分。
顶点色控制描边,存储平滑法线和描边宽度。



头发

效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
材质中将刘海单独提出来处理面部阴影和眉眼透视。
法线存的球形法线
uv0用来采diffuse图,uv1用来采hairSpec做能动的高光。



面部

效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
材质分成眉毛睫毛、眼睛、眼阴影、面部其他。
从uv0中可以看到眼睛是模拟物理做了视差采样。
uv1是面部采样sdf阴影的uv。


三、效果分类

单就veprin来说,可以将shader分成PBRBase、hair、fringe(英式用法,刘海)、face、eye、eye_blend、plush。


1. PBRBase:

漫反射和高光的Ramp
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

2. hair、fringe:

头发高光,刘海投影,眉眼透视
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

3. face:

SDF阴影,鼻尖嘴唇SDF高光,接受刘海投影。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

4. eye:

用视差计算,表现物理的折射和透射
效果还原-少女前线2:追放 vepley角色渲染分析

5. eye_blend:

一个是高光直接加上去,一个是阴影乘上去
效果还原-少女前线2:追放 vepley角色渲染分析

6. plush:

有动画,用多Pass毛发的做法来渲染
效果还原-少女前线2:追放 vepley角色渲染分析

四、效果实现

效果还原-少女前线2:追放 vepley角色渲染分析
很久前就记录了文档,多看几次又会有新理解。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
整理一下我写时候的框架,其中混入了PBRMask和ILMMask,这里的PBR还是常用的Cook-Torrance BRDF:
FragmentOutput frag(v2f i)
{
// Tex Sample
mainTex
pbrMask
bump
ilmMask

// VectorPrepare
lightDirWS
camDirWS
normalWS
NdotL
...

// Property prepare
emission
metallic
smoothness
...

// Remap NdotL ShadowArea
shadowArea calculate
NdotLRemap = 1 - shadowArea;
// TODO: Remap NdotV modify fresnel
NdotV
// shadowRamp
shadowRamp.rgb = Sample(_RampTex, 1 - shadowArea);
// Remap ShadowRamp , ILM SecondShadow
shadowRamp.rgb = lerp(_SecShadowColor.rgb, shadowRamp.rgb, ilm_AO);

// Direct PBR
NDF, G, F
// NDF GGX/Anisotropy specArea remap
NDF = NDF * ilm_SpecMask;
// SpecRamp
specRamp.rgb = Sample(_RampTex, specArea);
// Compose
directLightReuslt = (directDiffCol * shadowRamp + directSpecCol * specRamp) * mainlight.color * shadow * directOcclusion;

// Indirect PBR
// diffuse lerp Normal and Updir, lerp SHColor and self_envColor
indirDiff sampleSH RemapSHNormal
indirDiff * indirKd * albedo * occlusion
// specular use reflectionProbe or Cubemap or Matcap
indirSpec sampleCube
indirSpec * indirSpeFactor

// Other Emission, Rimlight, additional light

}

1. RampTex:

管线内实现在Danbaidong Shader GUI
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

2. PBRBase:

(1) PropertyPrepare

在PropertyPrepare阶段,计算shadowArea,将halfLambert结果用sigmoid函数将明暗交界线进行重新映射 (用smoothstep也可以),将结果作为NdotL进入PBR计算;用此结果作为shadowRamp的uv采样得到明暗交界线的颜色;

在这里得到的shadowRamp也可以根据ilm贴图去加上常暗阴影,这样只有受平行光区域给个常暗,灯光转到背面就整体都是一个阴影颜色,或者直接将diffuse去拉黑,这样受光不受光区域都是黑的,两种用法都有。
// Property prepare
half emission = 1 - mainTex.a;
half metallic = lerp(0, _Metallic, pbrMask.r);
half smoothness = lerp(0, _Smoothness, pbrMask.g);
half occlusion = lerp(1 - _Occlusion, 1, pbrMask.b);
half directOcclusion = lerp(1 - _DirectOcclusion, 1, pbrMask.b);
half3 albedo = mainTex.rgb * _BaseColor.rgb;
// NPR diffuse
float shadowArea = sigmoid(1 - halfLambert, _ShadowOffset, _ShadowSmooth * 10) * _ShadowStrength;
half3 shadowRamp = lerp(1, _ShadowColor.rgb, shadowArea);
//Remap NdotL for PBR Spec
half NdotLRemap = 1 - shadowArea;
#if _SHADOW_RAMP
shadowRamp = SampleDirectShadowRamp(TEXTURE2D_ARGS(_ShadowRampTex, sampler_ShadowRampTex), NdotLRemap);
#endif

// NdotV modify fresnel

// ilmShadow
shadowRamp.rgb = lerp(_SecShadowColor.rgb, shadowRamp.rgb, ilmAO);

(2) 直接光部分

在直接光照计算中计算DFG,其中D可以用贴图控制一下,除去菲涅尔的高光结果用来采specRampTex,将采样结果与原本高光叠加一下,最后混合乘上F项与shadowRamp的颜色。
// Direct
float3 directDiffColor = albedo.rgb;
float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
float roughness = max(PerceptualRoughnessToRoughness(perceptualRoughness), HALF_MIN_SQRT);
float roughnessSquare = max(roughness * roughness, HALF_MIN);
float3 F0 = lerp(0.04, albedo, metallic);
float NDF = DistributionGGX(NdotH, roughnessSquare);
float G = GeometrySmith(NdotLRemap, NdotV, pow(roughness + 1.0, 2.0) / 8.0);
float3 F = fresnelSchlick(HdotV, F0);

// GGX specArea remap
NDF = NDF * ilmSpecMask;
float3 kSpec = F;
// LightUpDiff: (1.0 - F) => (1.0 - F) * 0.5 + 0.5
float3 kDiff = ((1.0 - F) * 0.5 + 0.5) * (1.0 - metallic);
float3 nom = NDF * G * F;
float3 denom = 4.0 * NdotV * NdotLRemap + 0.0001;
float3 BRDFSpec = nom / denom;
directDiffColor = kDiff * albedo;
float3 directSpecColor = BRDFSpec * PI;
#if _SHADOW_RAMP
float specRange= saturate(NDF * G / denom.x);
half4 specRampCol = SampleDirectSpecularRamp(TEXTURE2D_ARGS(_ShadowRampTex, sampler_ShadowRampTex), specRange);
directSpecColor = clamp(specRampCol.rgb * 3 + BRDFSpec * PI / F, 0, 10) * F * shadowRamp;
#endif
// Compose direct lighting
float3 directLightResult = (directDiffColor * shadowRamp + directSpecColor * NdotLRemap)
* mainLight.color * mainLight.shadowAttenuation * directOcclusion;
效果还原-少女前线2:追放 vepley角色渲染分析
直接光结果
效果还原-少女前线2:追放 vepley角色渲染分析
高光及金属表现

(3) 环境光部分

添加自定义的环境光漫反射颜色,与本身环境光做一个lerp,受但又不全受环境影响,感觉之后也可以像蓝色协议那样算一个色值要好点。
环境光镜面反射也是同样道理。去加一个cubemap或matcap来作为环境光镜面反射的来源,也可以与本身的进行lerp。
但少前2这部分是不是没弄呀只用了个cubemap,不是很确定。
效果还原-少女前线2:追放 vepley角色渲染分析
day_indirDiff
效果还原-少女前线2:追放 vepley角色渲染分析
night_indirDiff
效果还原-少女前线2:追放 vepley角色渲染分析
cubemap
效果还原-少女前线2:追放 vepley角色渲染分析
specCube_0

(4) 叠加

计算结果叠加后,调一调可以得到游戏中差不多的效果
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

3. Hair、fringe:

(1) 头发高光
头发的uv排列整齐,区域形状是画好的,少前2这用的是根据uv进行上下移动来表现动态变化,还是用下雷老师的图示例~
效果还原-少女前线2:追放 vepley角色渲染分析
再来看看veprin头上的高光,应该就明白了这里是怎么做了,用个BlinnPhong算出高光再乘上mask,给个最小值来个基础色。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
// Hair Spec
float anisotropicOffsetV = - viewDirWS.y * _AnisotropicSlide + _AnisotropicOffset;
half3 hairSpecTex = SAMPLE_TEXTURE2D(_HairSpecTex, sampler_LinearClamp, float2(UV1.x, UV1.y + anisotropicOffsetV));
float hairSpecStrength = _SpecMinimum + pow(NdotH, _BlinnPhongPow) * NdotLRemap;
half3 hairSpecColor = hairSpecTex * _SpecColor * hairSpecStrength;

(2) 刘海投影

模板测试~
效果还原-少女前线2:追放 vepley角色渲染分析
角色0b_0110_0000
效果还原-少女前线2:追放 vepley角色渲染分析
FringeShadow 0b_0110_0001
角色正常绘制,finger在顶点着色中根据灯光方向进行偏移,不过相机视角在上面和下面的时候,y方向偏移的距离一样,但实际光照的时候我们在头顶看是看不到阴影的,所以这块加一个camDirFactor
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
// 本文中所用到的StencilUsage
//MaterialMask = 0b_1110_0000,
//...
//MaterialCharacterLit = 0b_0110_0000,
//MaterialCharFeatureMask = 0b_0110_0011,
//MaterialFringeShadow = 0b_0110_0001,
//MaterialEyelash = 0b_0110_0010,

Name "FringeShadowCaster"
Tags
{
"LightMode" = "GBufferFringeShadowCaster"
}
Stencil
{
Ref[_FriStencil]//MaterialCharacterLit
Comp[_FriStencilComp]//Equal
Pass[_FriStencilOp]//IncrementSaturate
ReadMask[_FriStencilReadMask]//MaterialFringeShadow
WriteMask[_FriStencilWriteMask]//MaterialFringeShadow
}
Cull Back
ZWrite Off
ColorMask [_FriColorMask]
...
FringeShadowCaster_v2f FringeShadowCasterVert(FringeShadowCaster_a2v v)
{
FringeShadowCaster_v2f o;
Light mainLight = GetMainLight();
float3 lightDirWS = normalize(mainLight.direction);
float3 lightDirVS = normalize(TransformWorldToViewDir(lightDirWS));
// Cam is Upward: let shadow close to face.
float3 camDirOS = normalize(TransformWorldToObject(GetCameraPositionWS()));
float camDirFactor = 1 - smoothstep(0.1, 0.9, camDirOS.y);
float3 positionVS = TransformWorldToView(TransformObjectToWorld(v.vertex));

positionVS.x -= 0.004 * lightDirVS.x * _ScreenOffsetScaleX;
positionVS.y -= 0.007 * _ScreenOffsetScaleY * camDirFactor;
o.positionHCS = TransformWViewToHClip(positionVS);
return o;
}
然后再渲一遍脸部,Face的shader中加一个Pass把匹配模板的地方都输出成阴影。
Name "FringeShadowReceiver"
Tags
{
"LightMode" = "GBufferFringeShadowReceiver"
}
Stencil
{
Ref[_FriStencil]//MaterialCharacterLit
Comp[_FriStencilComp]//Equal
Pass[_FriStencilOp]//Keep
ReadMask[_FriStencilReadMask]//MaterialFringeShadow
WriteMask[_FriStencilWriteMask]//MaterialFringeShadow
}
Cull Back
ZWrite Off
ColorMask [_FriColorMask]
效果还原-少女前线2:追放 vepley角色渲染分析
刘海投影
插一句,只用原模型来做只能这样了,但是跟我喜欢的效果不一样,我比较喜欢做成下面这种的阴影,加一个mesh片绑骨骼应该也可以也不耗,早就有这个做法。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

4. Face:

常规的SDF的阴影,渐变区域我也用的sigmoid:
效果还原-少女前线2:追放 vepley角色渲染分析
r: SDFshadowmap
效果还原-少女前线2:追放 vepley角色渲染分析
a: shadowMask
// vert
// Face lightmap dot value
Light mainLight = GetMainLight();
float3 lightDirWS = mainLight.direction;
lightDirWS.xz = normalize(lightDirWS.xz);
_FaceRightDirWS.xz = normalize(_FaceRightDirWS.xz);
o.faceLightDot.x = dot(lightDirWS.xz, _FaceRightDirWS.xz);
o.faceLightDot.y = saturate(dot(-lightDirWS.xz, _FaceFrontDirWS.xz) * 0.5 + _ShadowOffset);

// frag
// FaceLightMap
float2 faceLightMapUV = UV1;
faceLightMapUV.x = 1 - faceLightMapUV.x;
faceLightMapUV.x = i.faceLightDot.x < 0 ? 1 - faceLightMapUV.x : faceLightMapUV.x;
half4 faceLightMap = SAMPLE_TEXTURE2D(_FaceLightMap, sampler_FaceLightMap, faceLightMapUV);
half faceSDF = faceLightMap.r;
half faceShadowArea = faceLightMap.a;
float faceMapShadow = sigmoid(faceSDF, i.faceLightDot.y, _ShadowSmooth * 10) * faceShadowArea;
shadowArea = (1 - faceMapShadow) * _ShadowStrength;
鼻尖高光的部分是最神奇的:
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
单看嘴唇部分,应该是这两个一起表现的高光点的移动,易得是两个step结果相乘,能想到做成这样是真的牛批。与sdf阴影会叠一块不好看,可以选择加个smoothstep控一下。
// Nose Spec
float faceSpecStep = clamp(i.faceLightDot.y, 0.001, 0.999);
faceLightMapUV.x = 1 - faceLightMapUV.x;
faceLightMap = SAMPLE_TEXTURE2D(_FaceLightMap, sampler_FaceLightMap, faceLightMapUV);
float noseSpecArea1 = step(faceSpecStep, faceLightMap.g);
float noseSpecArea2 = step(1 - faceSpecStep, faceLightMap.b);
float noseSpecArea = noseSpecArea1 * noseSpecArea2;
// alternative: noseSpecArea *= smoothstep(_NoseSpecMin, _NoseSpecMax, 1 - i.faceLightDot.y)
效果还原-少女前线2:追放 vepley角色渲染分析
鼻尖高光点的移动
插一句,我这只将鼻尖高光加到了平行光照到的地方,感觉背光处也可以加一点淡淡的。脸颊高光点的地方是不是也能加上来。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

5. Eye、EyeBlend:

主体内陷的模型,不是单层外轮廓那种,可以加视差偏移uv加强一下效果也可以不加。
中间部分模型为高光,最外侧模型为阴影,都是透明进行叠加或混合。最外侧阴影这个效果真好,能根据视角移动产生变化。

效果还原-少女前线2:追放 vepley角色渲染分析
但这里我感觉还是单层外轮廓加视差偏移比较好,游戏中眼睛是没有透过头发的,如果要做透过的,就会出现头发把透明片挡住的情况。不是很明白这里的高光点为什么做成透明的,个人认为有两个改进的地方:
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
1. 这里和eyelash睫毛不知道为什么没把材质赋一起。一个透一个断了。
2. 眼睛高光和眼睛主体做一起走不透明的渲染。

// Eye
// Parallax
float3 viewDirOS = TransformWorldToObjectDir(viewDirWS);
viewDirOS = normalize(viewDirOS);
float2 parallaxOffset = viewDirOS.xy;
parallaxOffset.y *= -1;
float2 parallaxUV = i.uv + _ParallaxScale * parallaxOffset;
// parallaxMask
float2 centerVec = i.uv - float2(0.5, 0.5);
half centerDist = dot(centerVec, centerVec);
half parallaxMask = smoothstep(_ParallaxMaskEdge, _ParallaxMaskEdge + _ParallaxMaskEdgeOffset, 1 - centerDist);
// Tex Sample
half4 mainTex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, lerp(UV, parallaxUV, parallaxMask));
...

// Eye spec add
BlendOp [_BlendOp]// Add
Blend [_BlendSrc] [_BlendDst]// SrcAlpha One


// Eye shadow blend
BlendOp [_BlendOp]// Add
Blend [_BlendSrc] [_BlendDst]// SrcColor Zero

6. Plush:

普通的多Pass毛发,根据噪声图直接拉出顶点没有其他操作。
v2f vert (a2v v)
{
...
float offsetDist = _MULTIPASS_PARAMS.z * _FurLength;
float3 offsetPositionWS = TransformObjectToWorld(v.vertex);
float3 normalWS = TransformObjectToWorldNormal(v.normal);
offsetPositionWS += offsetDist * normalWS;
o.positionHCS = TransformWorldToHClip(offsetPositionWS);
o.positionWS = offsetPositionWS;
...
o.clipThreshold = _FurCLipMin + (_FurCLipMax - _FurCLipMin) * pow(_MULTIPASS_PARAMS.z, _FurPowShape);
return o;
}
FragmentOutput frag(v2f i)
{
UNITY_SETUP_INSTANCE_ID(i);
// Fur Clip
half furNoise = SAMPLE_TEXTURE2D(_FurNoise, sampler_FurNoise, i.uv.zw);
clip(furNoise - i.clipThreshold);
...
}

7. 管线设置:

在RenderGBuffer阶段插入的Pass
效果还原-少女前线2:追放 vepley角色渲染分析

五、其他实现

到这部分就不是纯还原游戏中的效果了,主要是边缘光和多光源的延迟着色计算。

1. 边缘光

如果要深入的话,一个是宽度控制,一个是跟阴影的结合,或是单独的边缘光光源。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
目前只是在延迟着色中,计算了个深度偏移边缘光,角色只做屏幕左右两边深度偏移,场景的话就向灯光方向。计算向光和背光区域,分别给个暖色和冷色。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析

2. 多光源

未做特殊处理,比如面部阴影、ramp等,最大的原因是延迟这块不好做,实在要做可以查表。另外我不喜欢油油的,把光滑度减弱了。

光照部分:
效果还原-少女前线2:追放 vepley角色渲染分析
边缘光向光源方向偏移。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
叠加后:
效果还原-少女前线2:追放 vepley角色渲染分析

3. 描边光照

要做好看的断裂采一张噪声就可以。
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
计算后发现还挺可以的就没有加噪声了:
效果还原-少女前线2:追放 vepley角色渲染分析
效果还原-少女前线2:追放 vepley角色渲染分析
多光源的描边:
效果还原-少女前线2:追放 vepley角色渲染分析
写法上不要纠结,就是NdotL截个范围。
// Outline
if ((data.materialFlags & kCharacterMaterialFlagOutline) != 0)
{
float lightMask = 1 - smoothstep(0.5, 0.8, abs(lightDirVS.z));
float outLineLightResult = step(0.8, NdotL * NdotL) * alpha * lightMask;
#if defined(_DIRECTIONAL)
return half4(unityLight.color * outLineLightResult, 1);
#else
outLineLightResult = step(0.8, pow(NdotL, 3)) * alpha;
return half4(unityLight.color * outLineLightResult, 1);
#endif
}

六、未实现

1. Transparent
2. Stocking
3. Environment shadow ramp
4. Face environment shadow only

七、参考文章

https://www.zhihu.com/people/zi-xie-42-53/posts
https://github.com/JasonMa0012/JTRP
https://zhuanlan.zhihu.com/p/57897827
https://zhuanlan.zhihu.com/p/361993606
https://zhuanlan.zhihu.com/p/434576854
© 版权声明

相关文章

暂无评论

暂无评论...