package org.openrndr.extra.dnk3 import org.openrndr.color.ColorRGBa import org.openrndr.draw.* import org.openrndr.math.Vector2 import org.openrndr.math.Vector3 import org.openrndr.math.Vector4 import org.openrndr.math.transforms.normalMatrix import java.nio.ByteBuffer import javax.naming.Context import kotlin.math.cos private val noise128 by lazy { val cb = colorBuffer(128, 128) val items = cb.width * cb.height * cb.format.componentCount val buffer = ByteBuffer.allocateDirect(items) for (y in 0 until cb.height) { for (x in 0 until cb.width) { for (i in 0 until 4) buffer.put((Math.random() * 255).toByte()) } } buffer.rewind() cb.write(buffer) cb.generateMipmaps() cb.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR) cb.wrapU = WrapMode.REPEAT cb.wrapV = WrapMode.REPEAT cb } private fun PointLight.fs(index: Int, hasNormalAttribute: Boolean): String = """ |{ | vec3 Lr = p_lightPosition$index - v_worldPosition; | float distance = length(Lr); | float attenuation = 1.0 / (p_lightConstantAttenuation$index + | p_lightLinearAttenuation$index * distance + p_lightQuadraticAttenuation$index * distance * distance); | vec3 L = normalize(Lr); | | float side = ${if (hasNormalAttribute) "dot(L, N)" else "3.1415"}; | f_diffuse += attenuation * max(0, side / 3.1415) * p_lightColor$index.rgb * m_color.rgb; | f_specular += attenuation * ggx(N, V, L, m_roughness, m_f0) * p_lightColor$index.rgb * m_color.rgb; } """.trimMargin() private fun AmbientLight.fs(index: Int): String = "f_ambient += p_lightColor$index.rgb * ((1.0 - m_metalness) * m_color.rgb);" private fun DirectionalLight.fs(index: Int, hasNormalAttribute: Boolean) = """ |{ | vec3 L = normalize(-p_lightDirection$index); | float attenuation = 1.0; | vec3 H = normalize(V + L); | float NoL = ${if (hasNormalAttribute) "clamp(dot(N, L), 0.0, 1.0)" else "1"}; | float LoH = clamp(dot(L, H), 0.0, 1.0); | float NoH = ${if (hasNormalAttribute) "clamp(dot(N, H), 0.0, 1.0)" else "1"}; | vec3 Lr = (p_lightPosition$index - v_worldPosition); //| vec3 L = normalize(Lr); | ${shadows.fs(index)} | | f_diffuse += NoL * attenuation * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb * m_ambientOcclusion;; | float Dg = D_GGX(m_roughness * m_roughness, NoH, H); | float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL); | vec3 F = F_Schlick(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); | vec3 Fr = (Dg * Vs) * F; | f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb * m_ambientOcclusion;; |} """.trimMargin() private fun HemisphereLight.fs(index: Int, hasNormalAttribute: Boolean): String = """ |{ | float f = ${if (hasNormalAttribute) "dot(N, p_lightDirection$index) * 0.5 + 0.5" else "1"}; | vec3 irr = ${irradianceMap?.let { "texture(p_lightIrradianceMap$index, N).rgb" } ?: "vec3(1.0)"}; | f_diffuse += mix(p_lightDownColor$index.rgb, p_lightUpColor$index.rgb, f) * irr * ((1.0 - m_metalness) * m_color.rgb) * m_ambientOcclusion; |} """.trimMargin() private fun SpotLight.fs(index: Int, hasNormalAttribute: Boolean): String { val shadows = shadows return """ |{ | vec3 Lr = p_lightPosition$index - v_worldPosition; | float distance = length(Lr); | float attenuation = 1.0 / (p_lightConstantAttenuation$index + | p_lightLinearAttenuation$index * distance + p_lightQuadraticAttenuation$index * distance * distance); | attenuation = 1.0; | vec3 L = normalize(Lr); | float NoL = ${if (hasNormalAttribute) "clamp(dot(N, L), 0.0, 1.0)" else "1"}; | float side = dot(L, N); | float hit = max(dot(-L, p_lightDirection$index), 0.0); | float falloff = clamp((hit - p_lightOuterCos$index) / (p_lightInnerCos$index - p_lightOuterCos$index), 0.0, 1.0); | attenuation *= falloff; | ${shadows.fs(index)} | { | vec3 H = normalize(V + L); | float LoH = clamp(dot(L, H), 0.0, 1.0); | float NoH = ${if (hasNormalAttribute) "clamp(dot(N, H), 0.0, 1.0)" else 1.0}; | f_diffuse += NoL * (0.1+0.9*attenuation) * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb ; | float Dg = D_GGX(m_roughness * m_roughness, NoH, H); | float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL); | vec3 F = F_Schlick(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); | vec3 Fr = (Dg * Vs) * F; | f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb; | } } """.trimMargin() } private fun Fog.fs(index: Int): String = """ |{ | float dz = min(1.0, -v_viewPosition.z/p_fogEnd$index); | f_fog = vec4(p_fogColor$index.rgb, dz); |} """.trimMargin() sealed class TextureSource object DummySource : TextureSource() { override fun toString(): String { return "DummySource()" } } abstract class TextureFromColorBuffer(var texture: ColorBuffer, var textureFunction: TextureFunction) : TextureSource() class TextureFromCode(val code: String) : TextureSource() private fun TextureFromCode.fs(index: Int, target: TextureTarget) = """ |vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0); |{ |vec4 texOut; |$code; |tex$index = texOut; |} """ enum class TextureFunction(val function: (String, String) -> String) { TILING({ texture, uv -> "texture($texture, $uv)" }), NOT_TILING({ texture, uv -> "textureNoTile(p_textureNoise, $texture, x_noTileOffset, $uv)" }) } /** * @param texture the texture to sample from * @param input input coordinates, default is "va_texCoord0.xy" * @param textureFunction the texture function to use, default is TextureFunction.TILING * @param pre the pre-fetch shader code to inject, can only adjust "x_texCoord" * @param post the post-fetch shader code to inject, can only adjust "x_texture" */ class ModelCoordinates(texture: ColorBuffer, var input: String = "va_texCoord0.xy", var tangentInput: String? = null, textureFunction: TextureFunction = TextureFunction.TILING, var pre: String? = null, var post: String? = null) : TextureFromColorBuffer(texture, textureFunction) { override fun toString(): String { return "ModelCoordinates(texture: $texture, input: $input, $tangentInput: $tangentInput, textureFunction: $textureFunction, pre: $pre, post: $post)" } } class Triplanar(texture: ColorBuffer, var scale: Double = 1.0, var offset: Vector3 = Vector3.ZERO, var sharpness: Double = 2.0, textureFunction: TextureFunction = TextureFunction.TILING, var pre: String? = null, var post: String? = null) : TextureFromColorBuffer(texture, textureFunction) { init { texture.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR) texture.wrapU = WrapMode.REPEAT texture.wrapV = WrapMode.REPEAT } } private fun ModelCoordinates.fs(index: Int) = """ |vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0); |{ | vec2 x_texCoord = $input; | vec2 x_noTileOffset = vec2(0.0); | vec4 x_texture; | ${if (pre != null) "{ $pre } " else ""} | x_texture = ${textureFunction.function("p_texture$index", "x_texCoord")}; | ${if (post != null) "{ $post } " else ""} | ${if (tangentInput != null) { """ | vec3 normal = normalize(va_normal.xyz); | vec3 tangent = normalize(${tangentInput}.xyz); | vec3 bitangent = cross(normal, tangent) * ${tangentInput}.w; | mat3 tbn = mat3(tangent, bitangent, normal); | x_texture.rgb = tbn * normalize(x_texture.rgb - vec3(0.5, 0.5, 0.)) ; | """.trimMargin() } else ""} | tex$index = x_texture; |} """.trimMargin() private fun Triplanar.fs(index: Int, target: TextureTarget) = """ |vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0); |{ | vec3 x_normal = va_normal; | vec3 x_position = va_position; | float x_scale = p_textureTriplanarScale$index; | vec3 x_offset = p_textureTriplanarOffset$index; | vec2 x_noTileOffset = vec2(0.0); | ${if (pre != null) "{ $pre } " else ""} | vec3 n = normalize(x_normal); | vec3 an = abs(n); | vec2 uvY = x_position.xz * x_scale + x_offset.x; | vec2 uvX = x_position.zy * x_scale + x_offset.y; | vec2 uvZ = x_position.xy * x_scale + x_offset.z; | vec4 tY = ${textureFunction.function("p_texture$index", "uvY")}; | vec4 tX = ${textureFunction.function("p_texture$index", "uvX")}; | vec4 tZ = ${textureFunction.function("p_texture$index", "uvZ")}; | vec3 weights = pow(an, vec3(p_textureTriplanarSharpness$index)); | weights = weights / (weights.x + weights.y + weights.z); | tex$index = tX * weights.x + tY * weights.y + weights.z * tZ; | ${if (target == TextureTarget.NORMAL) """ | vec3 tnX = normalize( tX.xyz - vec3(0.5, 0.5, 0.0)); | vec3 tnY = normalize( tY.xyz - vec3(0.5, 0.5, 0.0)) * vec3(1.0, -1.0, 1.0); | vec3 tnZ = normalize( tZ.xyz - vec3(0.5, 0.5, 0.0)); | vec3 nX = vec3(0.0, tnX.yx); | vec3 nY = vec3(tnY.x, 0.0, tnY.y); | vec3 nZ = vec3(tnZ.xy, 0.0); | vec3 normal = normalize(nX * weights.x + nY * weights.y + nZ * weights.z + n); | tex$index = vec4(normal, 0.0); """.trimMargin() else ""} |} ${if (post != null) """ vec4 x_texture = tex$index; { $post } tex$index = x_texture; """.trimIndent() else ""} """.trimMargin() sealed class TextureTarget(val name: String) { object NONE : TextureTarget("NONE") object COLOR : TextureTarget("COLOR") object ROUGHNESS : TextureTarget("ROUGHNESS") object METALNESS : TextureTarget("METALNESS") object METALNESS_ROUGHNESS : TextureTarget("METALNESS_ROUGHNESS") object EMISSION : TextureTarget("EMISSION") object NORMAL : TextureTarget("NORMAL") object AMBIENT_OCCLUSION : TextureTarget("AMBIENT_OCCLUSION") class Height(var scale: Double = 1.0) : TextureTarget("Height") override fun toString(): String { return "TextureTarget(name: $name)" } } class Texture(var source: TextureSource, var target: TextureTarget) { fun copy(): Texture { val copied = Texture(source, target) return copied } override fun toString(): String { return "Texture(source: $source, target: $target)" } } private var fragmentIDCounter = 1 class PBRMaterial : Material { override fun toString(): String { return "PBRMaterial(textures: $textures, color: $color, metalness: $metalness, roughness: $roughness, emissive: $emission))" } override var fragmentID = fragmentIDCounter.apply { fragmentIDCounter++ } override var doubleSided: Boolean = false override var transparent: Boolean = false var environmentMap = false var color = ColorRGBa.WHITE var metalness = 0.5 var roughness = 1.0 var emission = ColorRGBa.BLACK var fragmentPreamble: String? = null var vertexPreamble: String? = null var vertexTransform: String? = null var parameters = mutableMapOf() var textures = mutableListOf() val shadeStyles = mutableMapOf() // fun copy(): PBRMaterial { // val copied = PBRMaterial() // copied.environmentMap = environmentMap // copied.color = color // copied.opacity = opacity // copied.metalness = metalness // copied.roughness = roughness // copied.emission = emission // copied.vertexPreamble = vertexPreamble // copied.vertexTransform = vertexTransform // copied.parameters.putAll(parameters) // copied.textures.addAll(textures.map { it.copy() }) // return copied // } override fun generateShadeStyle(materialContext: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle { val cached = shadeStyles.getOrPut(ContextKey(materialContext, primitiveContext)) { val needLight = needLight(materialContext) val preambleFS = """ vec4 m_color = p_color; uint f_fragmentID = uint(p_fragmentID); float m_f0 = 0.5; float m_roughness = p_roughness; float m_metalness = p_metalness; float m_ambientOcclusion = 1.0; vec3 m_emission = p_emission.rgb; vec3 m_normal = vec3(0.0, 0.0, 1.0); vec4 f_fog = vec4(0.0, 0.0, 0.0, 0.0); vec3 f_worldNormal = v_worldNormal; """.trimIndent() val textureFs = if (needLight) { (textures.mapIndexed { index, it -> when (val source = it.source) { DummySource -> "vec4 tex$index = vec4(1.0);" is ModelCoordinates -> source.fs(index) is Triplanar -> source.fs(index, it.target) is TextureFromCode -> source.fs(index, it.target) else -> TODO() } } + textures.mapIndexed { index, texture -> when (texture.target) { TextureTarget.NONE -> "" TextureTarget.COLOR -> "m_color.rgb *= pow(tex$index.rgb, vec3(2.2)); m_color.a *= tex$index.a;" TextureTarget.METALNESS -> "m_metalness = tex$index.r;" TextureTarget.ROUGHNESS -> "m_roughness = tex$index.r;" TextureTarget.METALNESS_ROUGHNESS -> "m_metalness = tex$index.r; m_roughness = tex$index.g;" TextureTarget.EMISSION -> "m_emission *= tex$index.rgb;" TextureTarget.NORMAL -> "f_worldNormal = normalize((v_modelNormalMatrix * vec4(tex$index.xyz,0.0)).xyz);" TextureTarget.AMBIENT_OCCLUSION -> "m_ambientOcclusion *= tex$index.r;" is TextureTarget.Height -> "" } }).joinToString("\n") } else "" val displacers = textures.filter { it.target is TextureTarget.Height } val skinVS = if (primitiveContext.hasSkinning) """ uvec4 j = a_joints; mat4 skinTransform = p_jointTransforms[j.x] * a_weights.x + p_jointTransforms[j.y] * a_weights.y + p_jointTransforms[j.z] * a_weights.z + p_jointTransforms[j.w] * a_weights.w; ${if (primitiveContext.hasNormalAttribute) """ x_normal = normalize(mat3(skinTransform) * x_normal); """.trimIndent() else ""} x_position = (skinTransform * vec4(x_position,1)).xyz; """.trimIndent() else "" val textureVS = if (displacers.isNotEmpty()) textures.mapIndexed { index, it -> if (it.target is TextureTarget.Height) { when (val source = it.source) { DummySource -> "vec4 tex$index = vec4(1.0);" is ModelCoordinates -> source.fs(index) is Triplanar -> source.fs(index, it.target) is TextureFromCode -> source.fs(index, it.target) else -> TODO() } + """ x_position += x_normal * tex$index.r * p_textureHeightScale$index; """.trimIndent() } else "" }.joinToString("\n") else "" val lights = materialContext.lights val lightFS = if (needLight) """ vec3 f_diffuse = vec3(0.0); vec3 f_specular = vec3(0.0); vec3 f_emission = m_emission; vec3 f_ambient = vec3(0.0); float f_occlusion = 1.0; vec3 N = normalize(f_worldNormal); vec3 ep = (p_viewMatrixInverse * vec4(0.0, 0.0, 0.0, 1.0)).xyz; vec3 Vr = ep - v_worldPosition; vec3 V = normalize(Vr); float NoV = ${if (primitiveContext.hasNormalAttribute) "abs(dot(N, V)) + 1e-5" else "1"}; ${if (environmentMap && materialContext.meshCubemaps.isNotEmpty() && primitiveContext.hasNormalAttribute) """ { vec2 dfg = PrefilteredDFG_Karis(m_roughness, NoV); vec3 sc = m_metalness * m_color.rgb + (1.0-m_metalness) * vec3(0.04); f_specular.rgb += sc * (texture(p_environmentMap, reflect(-V, normalize(f_worldNormal))).rgb * dfg.x + dfg.y) * m_ambientOcclusion; } """.trimIndent() else ""} ${lights.mapIndexed { index, (node, light) -> when (light) { is AmbientLight -> light.fs(index) is PointLight -> light.fs(index, primitiveContext.hasNormalAttribute) is SpotLight -> light.fs(index, primitiveContext.hasNormalAttribute) is DirectionalLight -> light.fs(index, primitiveContext.hasNormalAttribute) is HemisphereLight -> light.fs(index, primitiveContext.hasNormalAttribute) else -> TODO() } }.joinToString("\n")} ${materialContext.fogs.mapIndexed { index, (node, fog) -> fog.fs(index) }.joinToString("\n")} """.trimIndent() else "" val rt = RenderTarget.active val combinerFS = materialContext.pass.combiners.map { it.generateShader() }.joinToString("\n") val fs = preambleFS + textureFs + lightFS + combinerFS val vs = (this@PBRMaterial.vertexTransform ?: "") + textureVS + skinVS shadeStyle { fragmentPreamble = this@PBRMaterial.fragmentPreamble ?: "" vertexPreamble = """ $shaderNoRepetitionVert ${(this@PBRMaterial.vertexPreamble) ?: ""} """.trimIndent() fragmentPreamble += """ |$shaderLinePlaneIntersect |$shaderProjectOnPlane |$shaderSideOfPlane |$shaderGGX |$shaderVSM |$shaderNoRepetition """.trimMargin() this.suppressDefaultOutput = true this.vertexTransform = vs fragmentTransform = fs materialContext.pass.combiners.map { if (rt is ProgramRenderTarget || materialContext.pass === DefaultPass || materialContext.pass === DefaultOpaquePass || materialContext.pass == DefaultTransparentPass) { this.output(it.targetOutput, ShadeStyleOutput(0)) } else { val index = rt.colorAttachmentIndexByName(it.targetOutput) ?: error("no such attachment ${it.targetOutput}") val type = rt.colorBuffer(index).type val format = rt.colorBuffer(0).format this.output(it.targetOutput, ShadeStyleOutput(index, format, type)) } } } } return cached } private fun needLight(context: MaterialContext): Boolean { val needSpecular = context.pass.combiners.any { FacetType.SPECULAR in it.facets } val needDiffuse = context.pass.combiners.any { FacetType.DIFFUSE in it.facets } val needLight = needSpecular || needDiffuse return needLight } override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) { shadeStyle.parameter("emission", emission) shadeStyle.parameter("color", color) shadeStyle.parameter("metalness", metalness) shadeStyle.parameter("roughness", roughness) shadeStyle.parameter("fragmentID", fragmentID) parameters.forEach { (k, v) -> when (v) { is Double -> shadeStyle.parameter(k, v) is Int -> shadeStyle.parameter(k, v) is Vector2 -> shadeStyle.parameter(k, v) is Vector3 -> shadeStyle.parameter(k, v) is Vector4 -> shadeStyle.parameter(k, v) is BufferTexture -> shadeStyle.parameter(k, v) is ColorBuffer -> shadeStyle.parameter(k, v) else -> TODO("support ${v::class.java}") } } if (needLight(context)) { textures.forEachIndexed { index, texture -> when (val source = texture.source) { is TextureFromColorBuffer -> { shadeStyle.parameter("texture$index", source.texture) if (source.textureFunction == TextureFunction.NOT_TILING) { shadeStyle.parameter("textureNoise", noise128) } } } when (val source = texture.source) { is Triplanar -> { shadeStyle.parameter("textureTriplanarSharpness$index", source.sharpness) shadeStyle.parameter("textureTriplanarScale$index", source.scale) shadeStyle.parameter("textureTriplanarOffset$index", source.offset) } } if (texture.target is TextureTarget.Height) { val target = texture.target as TextureTarget.Height shadeStyle.parameter("textureHeightScale$index", target.scale) } } val lights = context.lights lights.forEachIndexed { index, (node, light) -> shadeStyle.parameter("lightColor$index", light.color) when (light) { is AmbientLight -> { } is PointLight -> { shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz) shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation) shadeStyle.parameter("lightLinearAttenuation$index", light.linearAttenuation) shadeStyle.parameter("lightQuadraticAttenuation$index", light.quadraticAttenuation) } is SpotLight -> { shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz) shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz) shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation) shadeStyle.parameter("lightLinearAttenuation$index", light.linearAttenuation) shadeStyle.parameter("lightQuadraticAttenuation$index", light.quadraticAttenuation) shadeStyle.parameter("lightInnerCos$index", cos(Math.toRadians(light.innerAngle))) shadeStyle.parameter("lightOuterCos$index", cos(Math.toRadians(light.outerAngle))) if (light.shadows is Shadows.MappedShadows) { context.shadowMaps[light]?.let { val look = light.view(node) shadeStyle.parameter("lightTransform$index", light.projection(it) * look) if (light.shadows is Shadows.DepthMappedShadows) { shadeStyle.parameter("lightShadowMap$index", it.depthBuffer ?: TODO()) } if (light.shadows is Shadows.ColorMappedShadows) { shadeStyle.parameter("lightShadowMap$index", it.colorBuffer(0)) } } } } is DirectionalLight -> { shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz) shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz) if (light.shadows is Shadows.MappedShadows) { context.shadowMaps[light]?.let { val look = light.view(node) shadeStyle.parameter("lightTransform$index", light.projection(it) * look) if (light.shadows is Shadows.DepthMappedShadows) { shadeStyle.parameter("lightShadowMap$index", it.depthBuffer ?: TODO()) } if (light.shadows is Shadows.ColorMappedShadows) { shadeStyle.parameter("lightShadowMap$index", it.colorBuffer(0)) } } } } is HemisphereLight -> { shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz) shadeStyle.parameter("lightUpColor$index", light.upColor) shadeStyle.parameter("lightDownColor$index", light.downColor) light.irradianceMap?.let { shadeStyle.parameter("lightIrradianceMap$index", it) } } } } context.fogs.forEachIndexed { index, (node, fog) -> shadeStyle.parameter("fogColor$index", fog.color) shadeStyle.parameter("fogEnd$index", fog.end) } } else { textures.forEachIndexed { index, texture -> if (texture.target is TextureTarget.Height) { when (val source = texture.source) { is TextureFromColorBuffer -> shadeStyle.parameter("texture$index", source.texture) } when (val source = texture.source) { is Triplanar -> { shadeStyle.parameter("textureTriplanarSharpness$index", source.sharpness) shadeStyle.parameter("textureTriplanarScale$index", source.scale) shadeStyle.parameter("textureTriplanarOffset$index", source.offset) } } val target = texture.target as TextureTarget.Height shadeStyle.parameter("textureHeightScale$index", target.scale) } } } } }