Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Marker code of the 'scatter-3D' example #87

Open
QhelDIV opened this issue Dec 15, 2023 · 5 comments
Open

Marker code of the 'scatter-3D' example #87

QhelDIV opened this issue Dec 15, 2023 · 5 comments

Comments

@QhelDIV
Copy link

QhelDIV commented Dec 15, 2023

Thanks for your book! The figures are amazing.
Since I am working in 3D and have to play around with point clouds a lot, I really want to know how the 'Scatter-3D' exmaple is made.
It seems the core part is the custom marker. The marker's fading shadow rings create a beautiful effect of 3D shading.
Can you share the code for creating this example? Thanks a lot!

scatter-3d 1

@rougier
Copy link
Owner

rougier commented Jan 9, 2024

Sorry for delay. This is actually a regular scatter plot but the trick is to render each point several times using the disc marker, from back to front. "Halo" is made of transparent black disc with larger radius and the highlight is made with a smaller shifted white disc. Quite slow and primite but it gives the illusion. I thought the code was available from https://github.com/rougier/matplotlib-3d but I did not find it. Where did you find the image ? I could give me some hints on where to search the code.

@QhelDIV
Copy link
Author

QhelDIV commented Jan 10, 2024

Hi, thank you for your reply!

The image comes from the gallery show cases in this repo's README.md, the url is: https://github.com/rougier/scientific-visualization-book/blob/master/images/scatter-3d.png

Thank you again!

@rougier
Copy link
Owner

rougier commented Jan 10, 2024

Cannot find the original but here is another similar on with a different technique:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PolyCollection, LineCollection


def frustum(left, right, bottom, top, znear, zfar):
    M = np.zeros((4, 4))
    M[0, 0] = +2.0 * znear / (right - left)
    M[2, 0] = (right + left) / (right - left)
    M[1, 1] = +2.0 * znear / (top - bottom)
    M[2, 1] = (top + bottom) / (top - bottom)
    M[2, 2] = -(zfar + znear) / (zfar - znear)
    M[3, 2] = -2.0 * znear * zfar / (zfar - znear)
    M[2, 3] = -1.0
    return M.T

def perspective(fovy, aspect, znear, zfar):
    h = np.tan(fovy / 360.0 * np.pi) * znear
    w = h * aspect
    return frustum(-w, w, -h, h, znear, zfar)

def scale(x, y, z):
    return np.array([[x, 0, 0, 0],
                     [0, y, 0, 0],
                     [0, 0, z, 0],
                     [0, 0, 0, 1]], dtype=float)
def zoom(z):
    return scale(z,z,z)

def translate(x, y, z):
    return np.array([[1, 0, 0, x],
                     [0, 1, 0, y],
                     [0, 0, 1, z],
                     [0, 0, 0, 1]], dtype=float)

def xrotate(theta):
    t = np.pi * theta / 180
    c, s = np.cos(t), np.sin(t)
    return np.array([[1, 0,  0, 0],
                     [0, c, -s, 0],
                     [0, s,  c, 0],
                     [0, 0,  0, 1]], dtype=float)

def yrotate(theta):
    t = np.pi * theta / 180
    c, s = np.cos(t), np.sin(t)
    return  np.array([[ c, 0, s, 0],
                      [ 0, 1, 0, 0],
                      [-s, 0, c, 0],
                      [ 0, 0, 0, 1]], dtype=float)

def zrotate(theta):
    t = np.pi * theta / 180
    c, s = np.cos(t), np.sin(t)
    return np.array([[ c, -s, 0, 0],
                     [ s,  c, 0, 0],
                     [ 0,  0, 1, 0],
                     [ 0,  0, 0, 1]], dtype=float)


n = 11
V = np.zeros((12,n,3))
V[0] = [(x,0,0) for x in np.linspace(0,1,n)] # (0,0,0) to (1,0,0)
V[1] = [(x,0,1) for x in np.linspace(0,1,n)] # (0,0,1) to (1,0,1)
V[2] = [(0,0,z) for z in np.linspace(0,1,n)] # (0,0,0) to (0,0,1)
V[3] = [(1,0,z) for z in np.linspace(0,1,n)] # (1,0,0) to (1,0,1)
V[4] = [(0,y,0) for y in np.linspace(0,1,n)] # (0,0,0) to (0,1,0)
V[5] = [(1,y,0) for y in np.linspace(0,1,n)] # (1,0,0) to (1,1,0)
V[6] = [(x,1,0) for x in np.linspace(0,1,n)] # (0,1,0) to (1,1,0)
V[7] = [(1,y,1) for y in np.linspace(0,1,n)] # (1,0,0) to (1,1,0)
V[8] = [(1,1,z) for z in np.linspace(0,1,n)] # (0,1,0) to (1,1,0)

V[9]  = [(x,0,1.025) for x in np.linspace(0,1,n)]
V[10] = [(-0.025,0,z) for z in np.linspace(0,1,n)]
V[11] = [(-0.025,y,0) for y in np.linspace(0,1,n)]

V = V - 0.5


model = xrotate(25) @ yrotate(45) 
view  = translate(0, 0,-3.5)
proj  = perspective(30, 1, 1, 100) 
MVP   = proj  @ view  @ model 

def transform(V, depth=False):
    V = np.asarray(V).reshape(-1,3)
    VH = np.c_[V, np.ones(len(V))]     # Homogenous coordinates
    VT = (VH @ MVP.T)                  # Transformed coordinates
    VN = VT/abs(VT[:,3].reshape(-1,1)) # Normalization
    if depth:
        return VN[:,:2], VN[:,2]
    else:
        return VN[:,:2]
    
V = transform(V).reshape((12,n,2))

# 3 axis + grid (at once)
segments, linewidths = [], []
for i in range(0,n):
    if 0 < i < (n-1): linewidth = 0.25
    else:             linewidth = 1.00
    for (j,k) in [(0,1), (2,3), (4,5), (6,0), (7,5), (8,3)]:
        triangle = [V[j,i], V[k,i]]
        segments.append(triangle)
        linewidths.append(linewidth)

# Ticks
for i in range(0,n):
    triangle = [V[1,i], V[9,i]]
    segments.append(triangle)
    linewidths.append(1.0)

    triangle = [V[2,i], V[10,i]]
    segments.append(triangle)
    linewidths.append(1.0)

    triangle = [V[4,i], V[11,i]]
    segments.append(triangle)
    linewidths.append(1.0)



    
fig = plt.figure(figsize=(6,6))
ax = fig.add_axes([0,0,1,1], xlim=[-1,1], ylim=[-1,1], aspect=1)
ax.axis("off")
collection = PolyCollection(segments, linewidths=linewidths, 
                            facecolors="None", edgecolor="black")
ax.add_collection(collection)

# Tick labels
P = [(x, -0.5,+0.55) for x in np.linspace(-0.5,0.5,n)]
for i,pos in enumerate(transform(P)):
    ax.text(pos[0], pos[1], "%d" % (i-10),
            ha="center", va="center", size="x-small")

P = [(-0.55, -0.5, z) for z in np.linspace(-0.5,0.5,n)]
for i,pos in enumerate(transform(P)):
    ax.text(pos[0], pos[1], "%d" % i,
            ha="center", va="center", size="x-small")

P = [(-0.55, y, -0.50) for y in np.linspace(-0.5,0.5,n)]
for i,pos in enumerate(transform(P)):
    ax.text(pos[0], pos[1], "%d" % i,
            ha="center", va="center", size="x-small")


normalize = lambda x: np.asarray(x)/np.linalg.norm(x)
n = 256
I = np.zeros((n,n,4))
X,Y = np.linspace(-1,1,n), np.linspace(-1,1,n)
color = np.array([1,0,0])
white = np.array([1,1,1])
direction = normalize([1,1,1])
for i,x in enumerate(X):
    for j,y in enumerate(Y):
        z = 1 - np.sqrt(x*x+y*y)
        if z < 0: continue
        normal = normalize([x,y,z])
        diffuse = max(0,(direction*normal).sum())
        specular = np.power(diffuse,24)
        I[j,i,:3] = np.maximum(diffuse*color, specular)
        I[j,i,3] = 1


from matplotlib.offsetbox import OffsetImage, AnnotationBbox


mol = np.load("protein.npy")
V = mol["position"]
Z = mol["radius"]
V = (2*(V-V.min())/(V.max()-V.min()) - 1 )*.75
V = np.random.normal(0, .2, (1500,3))


V,Z = transform(V, depth=True)
zmin,zmax = Z.min(), Z.max()
Z = (Z-zmin)/(zmax-zmin)
V = V[np.argsort(-Z)]
for i in range(len(V)):
    image = OffsetImage(I, zoom=0.05, #*Z[i],
                        origin="lower", interpolation="nearest")
    box = AnnotationBbox(image, V[i], frameon=False)
    ax.add_artist(box)

plt.show()

@rougier
Copy link
Owner

rougier commented Jan 10, 2024

@QhelDIV
Copy link
Author

QhelDIV commented Jan 10, 2024

Thank you so much! I can directly render new point sets without any problem :)

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants