4

Is it possible to make a shape that passes through light so you can see through it with the light bent due to refraction? Like a lens or a glass (or water)?

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
  • 1
    You'll need to create your own shaders. The physics is not that tough. – Vatsal Manot Jul 07 '15 at 18:07
  • Thanks! Put in an answer -- The physics is trivial -- how hard is it to make a shader? I'm a 3D and SceneKit ultra-noob. – Lou Franco Jul 07 '15 at 18:13
  • For 2D scenes, it should be trivial. I'm not sure about 3D scenes though. – Vatsal Manot Jul 07 '15 at 18:26
  • 1
    I've answered your question (tl;dr: it's a yes). Speaking from personal experience, I've found that questions like these do not really yield the desired answers (yes/no is never enough!). For example, this question would be better titled as "How can I achieve refraction in SceneKit?". Remember, if it's not possible, people will let you know ;) – Vatsal Manot Jul 07 '15 at 18:34
  • were you able to achieve glass or water effect? if so please post an answer. for reference http://stackoverflow.com/questions/40693242/scenekit-and-with-glsl-how-to-add-shader-glsl-to-a-geometry – Hashmat Khalil Nov 20 '16 at 15:29
  • GLSL has a [refract](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/refract.xhtml) function that should allow this when you write your own shader. Sadly, I haven't yet figured out a working Metal/Scenekit shader for this. – Bersaelor Dec 28 '18 at 11:57

2 Answers2

9

To achieve refraction with SceneKit, you will need a SCNProgram. The built in shaders cannot do anything with refraction.

Based on the answer in this article (Which are the right Matrix Values to use in a metal shader passed by a SCNProgram to get a correct chrome like reflection) a refraction effect with SceneKit can be achieved like this. (This example is based on ARKit)

You need:

  • Swift
  • SceneKit / ARKit
  • SkyBox (we need something to Refract and Reflect)
  • SCNProgram
  • Metal Shaders
  • Physical Device (Recommended)

Make a fresh ARKit (SceneKit) project, create or load a sphere-like geometry object and put it in space. (SCNSphere will be okay)

Implement the skybox. Make sure your skybox composes of 6 individual images (Cube Map) - do not use 2:1 sphere maps, they seem not to work with the sampler in the metal shader. Here is a good link for skyboxes: (https://www.humus.name)

Make a the six required UIImages that holds the individual pictures for the skybox like so:

let skybox1 = UIImage.init(named: "art.scnassets/subdir/image-PX.png")
let skybox2 = UIImage.init(named: "art.scnassets/subdir/image-NX.png")
let skybox3 = UIImage.init(named: "art.scnassets/subdir/image-PY.png")
let skybox4 = UIImage.init(named: "art.scnassets/subdir/image-NY.png")
let skybox5 = UIImage.init(named: "art.scnassets/subdir/image-PZ.png")
let skybox6 = UIImage.init(named: "art.scnassets/subdir/image-NZ.png")

Images must be squares, and should be a power of 2 (for optimal mipmapping purposes). 512x512 will be good, 1024x1024 requires already a lot of memory

Make a SCNMaterialProperty that holds the array of individual images for the skybox like so:

// Cube-Map Structure:
//      PY
//  NX  PZ  PX  NZ
//      NY

// Array Order:
// PX, NX, PY, NY, PZ, NZ

let envMapSkyboxMaterialProperty = SCNMaterialProperty(contents: [skybox1!,skybox2!,skybox3!,skybox4!,skybox5!,skybox6!])

(P = positive, N = negative)

Then set the Skybox like so: (We need this for the reflective, refractive background and lighting of the Scene*)

myScene.background.contents = envMapSkyboxMaterialProperty?.contents

Set the Lighting environment as well**.

myScene.lightingEnvironment.contents = envMapSkyboxMaterialProperty?.contents

Assuming, that by now you can put your geometry object in space with a default material - we are now ready to line up the SCNProgram, with the special Metal Shaders for light refraction.

Make the SCNProgram and configure it like so:

let sceneProgramRefract = SCNProgram()
sceneProgramRefract.vertexFunctionName   = "myVertexRefract" // (myVertexRefract is the Keyword used in the shader)
sceneProgramRefract.fragmentFunctionName = "myFragmentRefract" // (myFragmentRefract is the Keyword used in the shader)

On your target Geometry-Node’s material attach the SCNProgram like so:

firstMaterial.program = sceneProgramRefract // doing this will replace the entire built-in SceneKit shaders for that object.
firstMaterial.setValue(envMapSkyboxMaterialProperty, forKey: "cubeTexture") // (cubeTexture is the Keyword used in the shader to access the Skybox)

Add a new Metal file to your project and call it “shaders.metal”

Replace anything in the Metal file with this:

// Default Metal Header for SCNProgram
#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>

// Default Sampler for the Skybox
constexpr sampler cubeSampler;


// Nodebuffer (you only need the enabled Matrix floats)
struct MyNodeBuffer {
    // float4x4 modelTransform;
    // float4x4 inverseModelTransform;
    float4x4 modelViewTransform; // required
    // float4x4 inverseModelViewTransform;
    float4x4 normalTransform; // required
    // float4x4 modelViewProjectionTransform;
    // float4x4 inverseModelViewProjectionTransform;
};

// Input Struct
typedef struct {
    float3 position [[ attribute(SCNVertexSemanticPosition) ]];
    float3 normal   [[ attribute(SCNVertexSemanticNormal)   ]];
} MyVertexInput;

// Struct filled by the Vertex Shader
struct SimpleVertexRefract
{
    float4 position [[position]];
    float  k;
    float3 worldSpaceReflection;
    float3 worldSpaceRefraction;
};

// VERTEX SHADER
vertex SimpleVertexRefract myVertexRefract(MyVertexInput in [[stage_in]],
                                          constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                          constant MyNodeBuffer& scn_node [[buffer(1)]])
{
float4 modelSpacePosition(in.position, 1.0f);
float4 modelSpaceNormal(in.normal, 0.0f);

// We'll be computing the reflection in eye space, so first we find the eye-space
// position. This is also used to compute the clip-space position below.
float4 eyeSpacePosition         = scn_node.modelViewTransform * modelSpacePosition;

// We compute the eye-space normal in the usual way.
float3 eyeSpaceNormal           = (scn_node.normalTransform * modelSpaceNormal).xyz;

// The view vector in eye space is just the vector from the eye-space position.
float3 eyeSpaceViewVector       = normalize(-eyeSpacePosition.xyz);

float3 view_vec                 = normalize(eyeSpaceViewVector);
float3 normal                   = normalize(eyeSpaceNormal);

const float ETA                 = 1.12f; // (this defines the intensity of the refraction. 1.0 will be no refraction)
float c                         = dot(view_vec, normal);
float d                         = ETA * c;
float k                         = clamp(d * d + (1.0f - ETA * ETA), 0.0f, 1.0f); // k is used in the fragment shader

// for Reflection / Refraction
// To find the reflection/refraction vector, we reflect/refract the (inbound) view vector about the normal.
float4 eyeSpaceReflection       = float4(reflect(-eyeSpaceViewVector, eyeSpaceNormal), 0.0f);
float4 eyeSpaceRefraction       = float4(refract(-eyeSpaceViewVector, eyeSpaceNormal, ETA), 0.0f);

// To sample the cube-map, we want a world-space reflection vector, so multiply
// by the inverse view transform to go back from eye space to world space.
float3 worldSpaceReflection     = (scn_frame.inverseViewTransform * eyeSpaceReflection).xyz;
float3 worldSpaceRefraction     = (scn_frame.inverseViewTransform * eyeSpaceRefraction).xyz;

// Fill the Out-Struct
SimpleVertexRefract out;
out.position                    = scn_frame.projectionTransform * eyeSpacePosition;
out.k                           = k;
out.worldSpaceReflection        = worldSpaceReflection; //
out.worldSpaceRefraction        = worldSpaceRefraction; //
return out;
}

// FRAGMENT SHADER
fragment float4 myFragmentRefract(SimpleVertexRefract in [[stage_in]],
                                  texturecube<float, access::sample> cubeTexture [[texture(0)]])
{
// Since the reflection vector's length will vary under interpolation, we normalize it
// and flip it from the assumed right-hand space of the world to the left-hand space
// of the interior of the cubemap.
float3 worldSpaceReflection     = normalize(in.worldSpaceReflection) * float3(1.0f, 1.0f, -1.0f);
float3 worldSpaceRefraction     = normalize(in.worldSpaceRefraction) * float3(1.0f, 1.0f, -1.0f);

float3 reflection               = cubeTexture.sample(cubeSampler, worldSpaceReflection).rgb;
float3 refraction               = cubeTexture.sample(cubeSampler, worldSpaceRefraction).rgb;

float4 color;
color.rgb                       = mix(reflection, refraction, float3(in.k)); // this is where k is finally used
color.a                         = 1.0f;
return color;
}

Compile and Run. The effect should look like this:

Figure 1

*If you use an AR Scene - setting the Skybox will overwrite the current camera feed, you might want to backup the AR feed elsewhere before you set the skybox, like so: Make a global definition:

var originalARSource : Any? = nil // screen Scene Backup
originalARSource = myScene.background.contents

You can jump back to the AR feed by setting myScene.background.contents back to originalARSource

** In ARKit make sure to set the Tracking Configuration to .none during the skybox is active:

configuration.environmentTexturing = .none
ZAY
  • 3,882
  • 2
  • 13
  • 21
  • keep in mind, that the refraction/reflection only works together with the skybox. if you put other SCNGeometry objects behind the refractive object, this other geometry will just become invisible. – ZAY Feb 04 '22 at 14:44
3

Yes, everything is possible with the amazing power of Physics! You'll need to create your own shader though. From Wikipedia:

In the field of computer graphics, a shader is a computer program that is used to do shading: the production of appropriate levels of color within an image, or, in the modern era, also to produce special effects or do video post-processing. A definition in layman's terms might be given as "a program that tells a computer how to draw something in a specific and unique way".

objc.io has a great tutorial on SceneKit if you're interested.

Vatsal Manot
  • 17,695
  • 9
  • 44
  • 80
  • Thanks -- I bought David Rönnqvist's book, but I'm not up to the chapter on shaders yet (just finished materials and thought it might be in there, but wasn't) – Lou Franco Jul 07 '15 at 18:44